runbios-mcp 0.2.1-dev.62
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/README.md +363 -0
- package/dist/api-client.d.ts +147 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +806 -0
- package/dist/api-client.js.map +1 -0
- package/dist/auth.d.ts +3 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +11 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +16 -0
- package/dist/config.js.map +1 -0
- package/dist/deployment-contract.d.ts +78 -0
- package/dist/deployment-contract.d.ts.map +1 -0
- package/dist/deployment-contract.js +155 -0
- package/dist/deployment-contract.js.map +1 -0
- package/dist/gpu-priorities.d.ts +25 -0
- package/dist/gpu-priorities.d.ts.map +1 -0
- package/dist/gpu-priorities.js +61 -0
- package/dist/gpu-priorities.js.map +1 -0
- package/dist/http/app.d.ts +14 -0
- package/dist/http/app.d.ts.map +1 -0
- package/dist/http/app.js +140 -0
- package/dist/http/app.js.map +1 -0
- package/dist/http/audit.d.ts +23 -0
- package/dist/http/audit.d.ts.map +1 -0
- package/dist/http/audit.js +41 -0
- package/dist/http/audit.js.map +1 -0
- package/dist/http/config.d.ts +23 -0
- package/dist/http/config.d.ts.map +1 -0
- package/dist/http/config.js +70 -0
- package/dist/http/config.js.map +1 -0
- package/dist/http/consent.d.ts +18 -0
- package/dist/http/consent.d.ts.map +1 -0
- package/dist/http/consent.js +148 -0
- package/dist/http/consent.js.map +1 -0
- package/dist/http/internal-auth.d.ts +22 -0
- package/dist/http/internal-auth.d.ts.map +1 -0
- package/dist/http/internal-auth.js +73 -0
- package/dist/http/internal-auth.js.map +1 -0
- package/dist/http/main.d.ts +3 -0
- package/dist/http/main.d.ts.map +1 -0
- package/dist/http/main.js +65 -0
- package/dist/http/main.js.map +1 -0
- package/dist/http/mcp-handler.d.ts +12 -0
- package/dist/http/mcp-handler.d.ts.map +1 -0
- package/dist/http/mcp-handler.js +103 -0
- package/dist/http/mcp-handler.js.map +1 -0
- package/dist/http/metadata.d.ts +6 -0
- package/dist/http/metadata.d.ts.map +1 -0
- package/dist/http/metadata.js +32 -0
- package/dist/http/metadata.js.map +1 -0
- package/dist/http/oauth.d.ts +56 -0
- package/dist/http/oauth.d.ts.map +1 -0
- package/dist/http/oauth.js +486 -0
- package/dist/http/oauth.js.map +1 -0
- package/dist/http/serviceHostGuard.d.ts +31 -0
- package/dist/http/serviceHostGuard.d.ts.map +1 -0
- package/dist/http/serviceHostGuard.js +68 -0
- package/dist/http/serviceHostGuard.js.map +1 -0
- package/dist/http/store.d.ts +150 -0
- package/dist/http/store.d.ts.map +1 -0
- package/dist/http/store.js +366 -0
- package/dist/http/store.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/inference-contract.d.ts +29 -0
- package/dist/inference-contract.d.ts.map +1 -0
- package/dist/inference-contract.js +112 -0
- package/dist/inference-contract.js.map +1 -0
- package/dist/redaction.d.ts +74 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +316 -0
- package/dist/redaction.js.map +1 -0
- package/dist/server.d.ts +63 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2282 -0
- package/dist/server.js.map +1 -0
- package/dist/training-contract.d.ts +107 -0
- package/dist/training-contract.d.ts.map +1 -0
- package/dist/training-contract.js +141 -0
- package/dist/training-contract.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +47 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,2282 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { basename, resolve } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { buildGPUOptionsParams, buildTrainingCreateBody, TRAINING_ADAPTERS, TRAINING_METHODS, trimTrainingLogs, } from "./training-contract.js";
|
|
7
|
+
import { buildDeploymentBody, buildDeploymentGPUOptionsParams, buildDeploymentPolicyBody, DEPLOYMENT_GPU_TIERS, DEPLOYMENT_SOURCE_TYPES, } from "./deployment-contract.js";
|
|
8
|
+
import { buildInferenceBody } from "./inference-contract.js";
|
|
9
|
+
import { redactBuildIdentity, redactBuildIdentityDeep, redactFreeTextFields, sanitizeArchitectureRegistry, stripBuildIdentity, } from "./redaction.js";
|
|
10
|
+
import { gpuRejectionCodeFor, gpuRejectionInstruction, gpuRejectionStatusFor, isPermanentGpuCode, } from "./api-client.js";
|
|
11
|
+
/**
|
|
12
|
+
* Job/deployment fields whose upstream value is RAW text rather than a curated
|
|
13
|
+
* sentence. `stop_last_error` is the proven one: the training stop saga stores
|
|
14
|
+
* the transport error from its last call to the machine verbatim, and that text
|
|
15
|
+
* is the request URL — a hostname that names the capacity supplier. The others
|
|
16
|
+
* are the same class (free text written from an upstream failure) and are
|
|
17
|
+
* covered so a future writer that stops curating cannot reopen the hole.
|
|
18
|
+
*/
|
|
19
|
+
const RAW_FAILURE_TEXT_FIELDS = [
|
|
20
|
+
"stop_last_error",
|
|
21
|
+
"error_message",
|
|
22
|
+
"last_error",
|
|
23
|
+
"stage_detail",
|
|
24
|
+
];
|
|
25
|
+
import { resolveLaunchGates } from "./config.js";
|
|
26
|
+
import { VERSION } from "./version.js";
|
|
27
|
+
export { VERSION };
|
|
28
|
+
/**
|
|
29
|
+
* Wrap a tool handler so hooks.onToolCall observes the tool name, success
|
|
30
|
+
* flag, elapsed milliseconds, and on failure the error message string only
|
|
31
|
+
* (never tool arguments). The wrapper does not change what the handler
|
|
32
|
+
* returns or throws; without a hook the handler is registered untouched.
|
|
33
|
+
*/
|
|
34
|
+
function wrapToolHandler(name, handler, onToolCall) {
|
|
35
|
+
if (!onToolCall)
|
|
36
|
+
return handler;
|
|
37
|
+
const fn = handler;
|
|
38
|
+
const wrapped = async (...args) => {
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
try {
|
|
41
|
+
const out = await fn(...args);
|
|
42
|
+
onToolCall({ tool: name, ok: true, ms: Date.now() - start });
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
onToolCall({
|
|
47
|
+
tool: name,
|
|
48
|
+
ok: false,
|
|
49
|
+
ms: Date.now() - start,
|
|
50
|
+
error: error instanceof Error ? error.message : String(error),
|
|
51
|
+
});
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return wrapped;
|
|
56
|
+
}
|
|
57
|
+
const SAFE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
58
|
+
function validateId(value, label) {
|
|
59
|
+
if (!SAFE_ID_RE.test(value)) {
|
|
60
|
+
throw new Error(`Invalid ${label}: must contain only alphanumeric characters, hyphens, and underscores`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function result(text) {
|
|
64
|
+
return { content: [{ type: "text", text }] };
|
|
65
|
+
}
|
|
66
|
+
function json(data) {
|
|
67
|
+
// A tool result of literally `null` is unreadable: an agent cannot tell an
|
|
68
|
+
// empty answer from a failure, and "null" is the one string that invites a
|
|
69
|
+
// wrong conclusion either way. The client already normalizes empty bodies;
|
|
70
|
+
// this is the last-resort guard for any other path.
|
|
71
|
+
if (data === null || data === undefined) {
|
|
72
|
+
return result(JSON.stringify({
|
|
73
|
+
code: "EMPTY_RESPONSE",
|
|
74
|
+
message: "The Run BiOS API answered this read with no JSON body. This is not an error verdict and nothing was changed.",
|
|
75
|
+
instruction: "Retry once; if it repeats, confirm the resource with the matching list/status tool before drawing a conclusion.",
|
|
76
|
+
}, null, 2));
|
|
77
|
+
}
|
|
78
|
+
return result(JSON.stringify(data, null, 2));
|
|
79
|
+
}
|
|
80
|
+
/* ─── Honest, actionable recovery text ───
|
|
81
|
+
* An MCP client ACTS on this text, so recovery advice must not dead-end on a
|
|
82
|
+
* single tool: list_models and search_models read the same registry through the
|
|
83
|
+
* same dependency, so one outage takes both down. Name every route the agent
|
|
84
|
+
* has, and tell it what to do when none of them answer, instead of pointing at
|
|
85
|
+
* one tool that may be unavailable. */
|
|
86
|
+
const CATALOG_RECOVERY = "Pick an id from the Run BiOS catalog: call list_models (everything hosted) or search_models (filter by name/provider) — "
|
|
87
|
+
+ "either one answers from the same registry. If BOTH are unavailable, do NOT guess another id: any id already returned "
|
|
88
|
+
+ "by list_inferences or list_training_jobs is known-hosted, the same catalog is on the Models page of the Run BiOS console, "
|
|
89
|
+
+ "and get_platform_guide (topic \"models\") states the rule. When nothing can confirm an id, report the catalog as "
|
|
90
|
+
+ "unavailable instead of retrying with an unverified one.";
|
|
91
|
+
/* ─── Context-length policy (owner-mandated, enforced server-side) ───
|
|
92
|
+
* The window is DERIVED unless the caller overrides it: the default is
|
|
93
|
+
* min(model native max, 262144), a model whose native window is unknown falls
|
|
94
|
+
* back to 32768, a genuinely small-context model keeps its own smaller window,
|
|
95
|
+
* and the hard ceiling is always the model's OWN native max — the server
|
|
96
|
+
* rejects anything above it. No static JSON Schema can express a per-model
|
|
97
|
+
* ceiling, so the bounds below are an honest sanity range and the description
|
|
98
|
+
* names the real authority (model.native_max_context from preflight). */
|
|
99
|
+
const CONTEXT_DEFAULT_CEILING = 262_144;
|
|
100
|
+
const CONTEXT_UNKNOWN_FALLBACK = 32_768;
|
|
101
|
+
const CONTEXT_TOOL_MIN = 1_024;
|
|
102
|
+
const CONTEXT_TOOL_MAX = 1_048_576;
|
|
103
|
+
const CONTEXT_POLICY_TEXT = `Omit it to accept the platform default: min(model native max, ${CONTEXT_DEFAULT_CEILING}) tokens, `
|
|
104
|
+
+ `falling back to ${CONTEXT_UNKNOWN_FALLBACK} when the model's native window is unknown (a model whose own window is `
|
|
105
|
+
+ `smaller keeps its own). The hard ceiling is the model's OWN native window — a larger value is rejected. `
|
|
106
|
+
+ `Read model.native_max_context from preflight_inference before proposing a value; `
|
|
107
|
+
+ `the ${CONTEXT_TOOL_MIN}-${CONTEXT_TOOL_MAX} range accepted here is only a sanity bound, never a per-model ceiling.`;
|
|
108
|
+
/**
|
|
109
|
+
* Context-length input with truthful bounds and self-contained rejection text.
|
|
110
|
+
* The MCP SDK renders a zod failure as its raw issue list, so the message on
|
|
111
|
+
* each bound has to carry the whole answer by itself — otherwise the caller
|
|
112
|
+
* gets a schema dump with no instruction.
|
|
113
|
+
*/
|
|
114
|
+
/* ─── Precision ───
|
|
115
|
+
* Precision is a sizing input, so it is a PRICE input: it sets the VRAM
|
|
116
|
+
* estimate, which sets the minimum GPU count, which sets the hourly total. An
|
|
117
|
+
* agent that writes "FP8" is asking for fp8, and must not be quoted the bf16
|
|
118
|
+
* price for it. Canonicalize case and whitespace before validating, then refuse
|
|
119
|
+
* anything outside the vocabulary BY NAME. bf16/fp16 are accepted spellings of
|
|
120
|
+
* "serve the checkpoint at its own precision", which is what none means.
|
|
121
|
+
*/
|
|
122
|
+
const QUANT_FORMATS = ["fp8", "awq", "gptq", "int4"];
|
|
123
|
+
const NATIVE_PRECISION_ALIASES = ["none", "bf16", "bfloat16", "fp16", "float16"];
|
|
124
|
+
function quantSchema(purpose) {
|
|
125
|
+
return z
|
|
126
|
+
.string()
|
|
127
|
+
.transform((v) => v.trim().toLowerCase())
|
|
128
|
+
.refine((v) => NATIVE_PRECISION_ALIASES.includes(v)
|
|
129
|
+
|| QUANT_FORMATS.includes(v), {
|
|
130
|
+
error: (issue) => `${String(issue.input)} is not a precision this platform can serve. `
|
|
131
|
+
+ `Use one of: ${QUANT_FORMATS.join(", ")}, or none.`,
|
|
132
|
+
})
|
|
133
|
+
.transform((v) => (NATIVE_PRECISION_ALIASES.includes(v) ? "none" : v))
|
|
134
|
+
.optional()
|
|
135
|
+
.describe(`${purpose} One of ${QUANT_FORMATS.join(", ")} or none. Case does not matter, `
|
|
136
|
+
+ "and bf16/fp16 mean the same as none. Anything else is refused by name rather "
|
|
137
|
+
+ "than sized at another precision.");
|
|
138
|
+
}
|
|
139
|
+
function contextLengthSchema(purpose) {
|
|
140
|
+
return z
|
|
141
|
+
.number()
|
|
142
|
+
.int({ error: "context_length must be a whole number of tokens." })
|
|
143
|
+
.min(CONTEXT_TOOL_MIN, {
|
|
144
|
+
error: (issue) => `context_length ${String(issue.input)} is below ${CONTEXT_TOOL_MIN} tokens, which cannot serve a request. `
|
|
145
|
+
+ CONTEXT_POLICY_TEXT,
|
|
146
|
+
})
|
|
147
|
+
.max(CONTEXT_TOOL_MAX, {
|
|
148
|
+
error: (issue) => `context_length ${String(issue.input)} is above the ${CONTEXT_TOOL_MAX}-token sanity bound of this tool. `
|
|
149
|
+
+ CONTEXT_POLICY_TEXT,
|
|
150
|
+
})
|
|
151
|
+
.optional()
|
|
152
|
+
.describe(`${purpose} ${CONTEXT_POLICY_TEXT}`);
|
|
153
|
+
}
|
|
154
|
+
/* ─── Spend-consent bounds (the one field that authorizes money) ───
|
|
155
|
+
* max_price_hour_cents is CONSENT, not a filter: it is the maximum total hourly
|
|
156
|
+
* price for the complete GPU count that the platform may charge without asking
|
|
157
|
+
* the user again. The server has no ceiling of its own — it only refuses a cap
|
|
158
|
+
* BELOW the primary placement's verified price (deployment-service
|
|
159
|
+
* validateInferenceGPUPriceContract, training-service validateTrainingRequest)
|
|
160
|
+
* — so `z.number().int()` alone advertised MAX_SAFE_INTEGER, i.e. told an agent
|
|
161
|
+
* that consenting to nine quadrillion cents an hour is a legal request. The
|
|
162
|
+
* ceiling below comes from what the platform can actually book: the most
|
|
163
|
+
* expensive placement in the GPU catalog is 8 x B300 at 1035 cents per GPU-hour
|
|
164
|
+
* = 8280 cents/hour total, and the beta serving cap (models up to ~130B total
|
|
165
|
+
* parameters) keeps a deployment inside that ladder. 100000 cents ($1,000/hour)
|
|
166
|
+
* is roughly twelve times the dearest bookable configuration: high enough that
|
|
167
|
+
* it can never reject a real cap, low enough that it is a limit rather than a
|
|
168
|
+
* blank cheque. It is a sanity bound, not a price quote — the authority is
|
|
169
|
+
* always the preflight response. */
|
|
170
|
+
const PRICE_CAP_TOOL_MAX = 100_000;
|
|
171
|
+
const priceCapPolicyText = (floor) => "This is SPEND CONSENT, not a filter: the maximum accepted TOTAL hourly price for the complete GPU count, in cents. "
|
|
172
|
+
+ "Copy the cap the user approved from the preflight response (billing.max_price_hour_cents, or the chosen placement's "
|
|
173
|
+
+ "total_price_hour_cents) — never invent one. The server rejects a cap BELOW the primary placement's current price, and "
|
|
174
|
+
+ `omitting it accepts the verified price of the placements already approved. The ${floor}-${PRICE_CAP_TOOL_MAX} range here `
|
|
175
|
+
+ "is a sanity bound on consent (the dearest configuration the GPU catalog can book is well under it), not a price quote and "
|
|
176
|
+
+ "not a per-model ceiling; get_gpu_pricing and preflight give the real numbers.";
|
|
177
|
+
/**
|
|
178
|
+
* Price-cap input with a defensible ceiling and self-contained rejection text.
|
|
179
|
+
* `allowZero` covers the training tools, where 0 is the documented "no explicit
|
|
180
|
+
* cap — accept the verified placement price" value; the deployment tools require
|
|
181
|
+
* a positive cap (deployment-contract.ts rejects <= 0).
|
|
182
|
+
*/
|
|
183
|
+
function priceCapSchema(allowZero) {
|
|
184
|
+
const floor = allowZero ? 0 : 1;
|
|
185
|
+
const policy = priceCapPolicyText(floor);
|
|
186
|
+
return z
|
|
187
|
+
.number()
|
|
188
|
+
.int({ error: "max_price_hour_cents must be a whole number of cents." })
|
|
189
|
+
.min(floor, {
|
|
190
|
+
error: (issue) => `max_price_hour_cents ${String(issue.input)} is below ${floor}. `
|
|
191
|
+
+ (allowZero
|
|
192
|
+
? "A cap cannot be negative; pass 0 (or omit it) to accept the verified placement price. "
|
|
193
|
+
: "Omit it instead of passing 0 or a negative number to accept the verified placement price. ")
|
|
194
|
+
+ policy,
|
|
195
|
+
})
|
|
196
|
+
.max(PRICE_CAP_TOOL_MAX, {
|
|
197
|
+
error: (issue) => `max_price_hour_cents ${String(issue.input)} is above the ${PRICE_CAP_TOOL_MAX}-cent/hour ($1,000/hour) sanity bound `
|
|
198
|
+
+ "of this tool. A cap that large is not a limit — it authorizes any price the platform could ever charge. "
|
|
199
|
+
+ policy,
|
|
200
|
+
})
|
|
201
|
+
.optional()
|
|
202
|
+
.describe(policy);
|
|
203
|
+
}
|
|
204
|
+
/* ─── Training context window (max_seq_length) ───
|
|
205
|
+
* Same defect as context_length before #789: `max(10_000_000)` is not a policy,
|
|
206
|
+
* it is a number nothing enforces. The authoritative contract is
|
|
207
|
+
* get_training_capabilities, whose `max_length` field states the platform's
|
|
208
|
+
* default (2048) and its floor (128); the real ceiling is the base model's own
|
|
209
|
+
* position-embedding window, which no static JSON Schema can express. So the
|
|
210
|
+
* schema advertises the contract's floor and the same sanity ceiling serving
|
|
211
|
+
* uses, and the description names the real authority. */
|
|
212
|
+
const TRAINING_SEQ_TOOL_MIN = 128;
|
|
213
|
+
const TRAINING_SEQ_DEFAULT = 2_048;
|
|
214
|
+
const TRAINING_SEQ_POLICY_TEXT = `Training sequence length in tokens (sent as config.max_length). Omit it to accept the platform default of ${TRAINING_SEQ_DEFAULT}. `
|
|
215
|
+
+ "The hard ceiling is the BASE MODEL's own context window — a longer value wastes GPU memory on padding and can fail the fit "
|
|
216
|
+
+ `check — and the authoritative field bounds live in get_training_capabilities (max_length). The ${TRAINING_SEQ_TOOL_MIN}-`
|
|
217
|
+
+ `${CONTEXT_TOOL_MAX} range accepted here is only a sanity bound, never a per-model ceiling: read the model's window from `
|
|
218
|
+
+ "get_model_config or preflight_training_job before proposing a value. Longer sequences raise memory use roughly linearly, "
|
|
219
|
+
+ "so raise it deliberately, not by default.";
|
|
220
|
+
const MAX_TOKENS_POLICY_TEXT = "Maximum tokens to GENERATE for this completion. Omit it to let the endpoint choose. The real ceiling is the deployment's "
|
|
221
|
+
+ `served context window MINUS the prompt (read context_length from get_inference_status or the create response); the 1-`
|
|
222
|
+
+ `${CONTEXT_TOOL_MAX} range accepted here is only a sanity bound, never the window of the model being called. Tokens are `
|
|
223
|
+
+ "billed, so ask for what the answer needs rather than the maximum.";
|
|
224
|
+
function trainingSeqLengthSchema() {
|
|
225
|
+
return z
|
|
226
|
+
.number()
|
|
227
|
+
.int({ error: "max_seq_length must be a whole number of tokens." })
|
|
228
|
+
.min(TRAINING_SEQ_TOOL_MIN, {
|
|
229
|
+
error: (issue) => `max_seq_length ${String(issue.input)} is below the ${TRAINING_SEQ_TOOL_MIN}-token floor of the training contract. `
|
|
230
|
+
+ TRAINING_SEQ_POLICY_TEXT,
|
|
231
|
+
})
|
|
232
|
+
.max(CONTEXT_TOOL_MAX, {
|
|
233
|
+
error: (issue) => `max_seq_length ${String(issue.input)} is above the ${CONTEXT_TOOL_MAX}-token sanity bound of this tool. `
|
|
234
|
+
+ TRAINING_SEQ_POLICY_TEXT,
|
|
235
|
+
})
|
|
236
|
+
.optional()
|
|
237
|
+
.describe(TRAINING_SEQ_POLICY_TEXT);
|
|
238
|
+
}
|
|
239
|
+
// Reuse this exact schema for preflight and create. The helper beneath it
|
|
240
|
+
// performs the source-dependent checks that a flat MCP input schema cannot.
|
|
241
|
+
const deploymentCreateToolSchema = {
|
|
242
|
+
name: z.string().min(1).max(255).describe("Deployment display name."),
|
|
243
|
+
source_type: z
|
|
244
|
+
.enum(DEPLOYMENT_SOURCE_TYPES)
|
|
245
|
+
.describe("Deploy a verified training checkpoint or a base model from the catalog."),
|
|
246
|
+
source_job_id: z
|
|
247
|
+
.string()
|
|
248
|
+
.optional()
|
|
249
|
+
.describe("Owning training job ID; required with source_type=checkpoint."),
|
|
250
|
+
source_checkpoint_id: z
|
|
251
|
+
.string()
|
|
252
|
+
.optional()
|
|
253
|
+
.describe("Verified checkpoint ID; required with source_type=checkpoint."),
|
|
254
|
+
hf_model_id: z
|
|
255
|
+
.string()
|
|
256
|
+
.optional()
|
|
257
|
+
.describe("Catalog model id (see list_models); required when deploying a hosted base model (source_type=hf_model, the legacy wire name). Must be hosted on Run BiOS."),
|
|
258
|
+
hf_model_revision: z
|
|
259
|
+
.string()
|
|
260
|
+
.regex(/^[0-9a-f]{40}$/)
|
|
261
|
+
.optional()
|
|
262
|
+
.describe("Exact model commit from preflight_inference canonical_request. Copy it unchanged into create_inference."),
|
|
263
|
+
hf_integration_id: z
|
|
264
|
+
.string()
|
|
265
|
+
.optional()
|
|
266
|
+
.describe("Legacy field kept for compatibility — catalog models are pre-mirrored and never gated, so no integration is needed. Raw tokens are not accepted by MCP."),
|
|
267
|
+
base_model_id: z
|
|
268
|
+
.string()
|
|
269
|
+
.optional()
|
|
270
|
+
.describe("Base model for an adapter checkpoint; normally resolved from training lineage."),
|
|
271
|
+
base_model_revision: z
|
|
272
|
+
.string()
|
|
273
|
+
.regex(/^[0-9a-f]{40}$/)
|
|
274
|
+
.optional()
|
|
275
|
+
.describe("Exact base-model commit from preflight_inference canonical_request. Copy it unchanged into create_inference."),
|
|
276
|
+
// serving_mode, model_task and supports_images are DELIBERATELY ABSENT.
|
|
277
|
+
// All three are derived server-side and immutable: the serving mode comes
|
|
278
|
+
// from the verified checkpoint's lineage, the OpenAI task from the resolved
|
|
279
|
+
// model (chat, or completion for a base model with no chat template, or
|
|
280
|
+
// embedding/rerank for those architectures), and image support from the
|
|
281
|
+
// model's own config. Advertising them as inputs invited exactly the 400s the
|
|
282
|
+
// create gate now returns ("configured automatically from the model itself
|
|
283
|
+
// ... and cannot be changed"). They are reported back on the
|
|
284
|
+
// preflight/create/status response, which is where a caller reads them.
|
|
285
|
+
gpu_type: z
|
|
286
|
+
.string()
|
|
287
|
+
.min(1)
|
|
288
|
+
.describe("Exact GPU SKU approved after reviewing authoritative deployment options."),
|
|
289
|
+
gpu_count: z
|
|
290
|
+
.number()
|
|
291
|
+
.int()
|
|
292
|
+
.min(1)
|
|
293
|
+
.max(8)
|
|
294
|
+
.describe("Tensor-parallel GPU count accepted by the model-fit option."),
|
|
295
|
+
gpu_priorities: z
|
|
296
|
+
.array(z.object({
|
|
297
|
+
gpu_type: z.string().min(1), gpu_count: z.number().int().min(1).max(8),
|
|
298
|
+
provider: z.string().min(1).max(30).optional(), region: z.string().min(1).max(64).optional(),
|
|
299
|
+
tier: z.literal("secure").optional(),
|
|
300
|
+
}))
|
|
301
|
+
.min(1)
|
|
302
|
+
.max(5)
|
|
303
|
+
.optional()
|
|
304
|
+
.describe("Ranked GPU placement choices. Copy these unchanged from the preflight response's accepted choices; the platform fills in and manages infrastructure placement automatically. Queueing requires 3-5 distinct entries; entry one must match gpu_type/gpu_count."),
|
|
305
|
+
gpu_tier: z
|
|
306
|
+
.enum(DEPLOYMENT_GPU_TIERS)
|
|
307
|
+
.optional()
|
|
308
|
+
.describe("Deployment serving supports secure capacity only; defaults to secure."),
|
|
309
|
+
allow_capacity_queue: z
|
|
310
|
+
.boolean()
|
|
311
|
+
.optional()
|
|
312
|
+
.describe("Explicit consent to wait up to seven days if this exact SKU is unavailable; defaults to false."),
|
|
313
|
+
max_price_hour_cents: priceCapSchema(false),
|
|
314
|
+
storage_gb: z.number().int().min(1).max(10000).optional(),
|
|
315
|
+
context_length: contextLengthSchema("Serving context window in tokens."),
|
|
316
|
+
quant: quantSchema("Runtime precision. Pre-quantized checkpoint precision may be locked."),
|
|
317
|
+
serving_config: z
|
|
318
|
+
.record(z.string(), z.unknown())
|
|
319
|
+
.optional()
|
|
320
|
+
.describe("Advanced serving arguments; the server applies its strict allowlist."),
|
|
321
|
+
};
|
|
322
|
+
const deploymentCreateMutationToolSchema = {
|
|
323
|
+
...deploymentCreateToolSchema,
|
|
324
|
+
idempotency_key: z
|
|
325
|
+
.string()
|
|
326
|
+
.regex(/^[A-Za-z0-9._:-]{8,128}$/)
|
|
327
|
+
.describe("Stable retry key. Reuse the same value with the unchanged payload after timeouts to recover the same deployment and one-time inference key."),
|
|
328
|
+
};
|
|
329
|
+
/* ─── Response shaping that keeps provenance honest ─── */
|
|
330
|
+
function asRecord(value) {
|
|
331
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
332
|
+
? value
|
|
333
|
+
: undefined;
|
|
334
|
+
}
|
|
335
|
+
function nonEmptyString(value) {
|
|
336
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
337
|
+
}
|
|
338
|
+
/* ─── Internal build references never leave the platform ─── */
|
|
339
|
+
/**
|
|
340
|
+
* Recursively redact every string in a JSON-shaped value, counting the fields
|
|
341
|
+
* that carried something.
|
|
342
|
+
*
|
|
343
|
+
* Redacting by KEY was defeated the moment the same value appeared under a
|
|
344
|
+
* different key: dropping `image_tag` left the identical
|
|
345
|
+
* `<registry>/<engine>@sha256:<digest>` in the free-text `notes` of 194
|
|
346
|
+
* training-scope rows, and the inference scope has the same shape waiting in
|
|
347
|
+
* the "dropped by engine <tag>" note its auto-disable pass writes. So the rule
|
|
348
|
+
* is content, not field name, and it runs over every string in the payload.
|
|
349
|
+
*
|
|
350
|
+
* The matching itself lives in redaction.ts and is NOT repeated here. An
|
|
351
|
+
* earlier revision of this module carried its own pattern list anchored on a
|
|
352
|
+
* plaintext array of the engine repositories Run BiOS builds — and `npm publish`
|
|
353
|
+
* ships dist/, so that array handed every customer the exact roster the
|
|
354
|
+
* redactor exists to protect (test/build-identity-leak.test.ts pins this, and
|
|
355
|
+
* the console hit the identical defect in its own leak guard). redaction.ts
|
|
356
|
+
* matches the same references by SHAPE plus salted hashes of the names, so it
|
|
357
|
+
* is strictly stronger AND publishes nothing.
|
|
358
|
+
*/
|
|
359
|
+
function redactDeep(value, counter) {
|
|
360
|
+
if (typeof value === "string") {
|
|
361
|
+
const out = redactBuildIdentity(value);
|
|
362
|
+
if (out !== value)
|
|
363
|
+
counter.hits += 1;
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
if (Array.isArray(value))
|
|
367
|
+
return value.map((entry) => redactDeep(entry, counter));
|
|
368
|
+
const record = asRecord(value);
|
|
369
|
+
if (!record)
|
|
370
|
+
return value;
|
|
371
|
+
const out = {};
|
|
372
|
+
for (const [key, entry] of Object.entries(record))
|
|
373
|
+
out[key] = redactDeep(entry, counter);
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Label the architecture registry's provenance instead of shipping it bare.
|
|
378
|
+
*
|
|
379
|
+
* Each row in the registry TABLE records image_tag/bios_version/manifest_sha256
|
|
380
|
+
* from the serving image whose capability manifest last published it. That is
|
|
381
|
+
* REGISTRY PROVENANCE, not the image the platform is running now, and the two
|
|
382
|
+
* drift apart whenever a newer image has not re-published the manifest. An
|
|
383
|
+
* agent that reads a build reference next to a support list concludes "the
|
|
384
|
+
* platform runs this build" and tells the user a stale version — a wrong action
|
|
385
|
+
* caused purely by presentation. The support decision itself IS authoritative
|
|
386
|
+
* (the deploy gate reads these same rows), so the rows are preserved; the
|
|
387
|
+
* internal image reference is removed by CONTENT from every field of every
|
|
388
|
+
* scope (see redactBuildReferences) and replaced with an explicit provenance
|
|
389
|
+
* block. Fixing the underlying manifest staleness is a control-plane concern
|
|
390
|
+
* and is deliberately NOT duplicated here.
|
|
391
|
+
*
|
|
392
|
+
* Those three columns no longer REACH this function: the public endpoint
|
|
393
|
+
* projects them away before the response leaves the control plane. What arrives
|
|
394
|
+
* here is the customer projection, and this block must describe that.
|
|
395
|
+
*
|
|
396
|
+
* The block itself only asserts what the rows actually carry, and since the
|
|
397
|
+
* control plane started projecting this endpoint that is LESS than it once was:
|
|
398
|
+
* image_tag, bios_version and manifest_sha256 are dropped at the boundary now
|
|
399
|
+
* (publicArchRow in deployment-service), so no response can still see them.
|
|
400
|
+
* Counting them here would therefore report 0 forever and drive the block to
|
|
401
|
+
* announce "no row records a build version" about a registry whose rows do —
|
|
402
|
+
* narrating a field that is empty on every row is the same defect one level up,
|
|
403
|
+
* and asserting a fact about rows this layer can no longer inspect is worse.
|
|
404
|
+
* `source` is the discriminator that survives the projection, so it is the one
|
|
405
|
+
* this block counts on.
|
|
406
|
+
*/
|
|
407
|
+
export function annotateArchitectureProvenance(data) {
|
|
408
|
+
const payload = asRecord(data);
|
|
409
|
+
if (!payload || !Array.isArray(payload.architectures))
|
|
410
|
+
return data;
|
|
411
|
+
const counter = { hits: 0 };
|
|
412
|
+
let manifestRows = 0;
|
|
413
|
+
let lastPublished = "";
|
|
414
|
+
const architectures = payload.architectures.map((entry) => {
|
|
415
|
+
const row = asRecord(entry);
|
|
416
|
+
if (!row)
|
|
417
|
+
return redactDeep(entry, counter);
|
|
418
|
+
// Belt and braces: the boundary already omits image_tag, so this destructure
|
|
419
|
+
// only matters if the projection ever regresses. It is cheap and it is the
|
|
420
|
+
// reason this file carries a REVIEWED entry in the leak-gate allowlist —
|
|
421
|
+
// deleting it means deleting that entry too, or the gate fails on a stale one.
|
|
422
|
+
const { image_tag: _internalImageReference, ...rest } = row;
|
|
423
|
+
if (nonEmptyString(row.source) === "manifest")
|
|
424
|
+
manifestRows += 1;
|
|
425
|
+
const updated = nonEmptyString(row.updated_at);
|
|
426
|
+
if (updated && updated > lastPublished)
|
|
427
|
+
lastPublished = updated;
|
|
428
|
+
return redactDeep(rest, counter);
|
|
429
|
+
});
|
|
430
|
+
// Every OTHER field of the envelope goes through the same content rule: the
|
|
431
|
+
// whole point is that no field name is trusted, including one the control
|
|
432
|
+
// plane has not added yet.
|
|
433
|
+
const { architectures: _rows, ...envelope } = payload;
|
|
434
|
+
return {
|
|
435
|
+
...redactDeep(envelope, counter),
|
|
436
|
+
architectures,
|
|
437
|
+
manifest_provenance: {
|
|
438
|
+
note: "Architecture support and the enabled flags are authoritative — the deployment gate reads these same rows. "
|
|
439
|
+
+ "Some rows were published by an internal build's capability manifest, and the count below says how many. WHICH "
|
|
440
|
+
+ "build is not part of this response at all: the platform does not publish it on this endpoint, so nothing here "
|
|
441
|
+
+ "names or numbers an engine build. Do not report a platform version from this response, and do not infer from "
|
|
442
|
+
+ "the absence that the registry is stale or current — this response cannot tell you either way. "
|
|
443
|
+
+ "Build identity is removed by key AND by content from every field, so nothing here names the engine the platform runs.",
|
|
444
|
+
// The COUNTS are the honest signal; the VALUES are not. An earlier revision
|
|
445
|
+
// of this block also listed `recorded_bios_versions` — the distinct engine
|
|
446
|
+
// build versions read off the rows — which handed the caller the exact
|
|
447
|
+
// build identity every other rule here removes. Counting says as much about
|
|
448
|
+
// staleness and names nothing. Its siblings that counted build versions and
|
|
449
|
+
// manifest digests are gone for the opposite reason: after the boundary
|
|
450
|
+
// projection they could only ever have counted 0.
|
|
451
|
+
rows: architectures.length,
|
|
452
|
+
rows_from_a_published_manifest: manifestRows,
|
|
453
|
+
last_row_update: lastPublished || null,
|
|
454
|
+
// Counts FIELDS that carried a reference, not individual references: the
|
|
455
|
+
// matcher rewrites a whole string in one pass, so a per-occurrence tally
|
|
456
|
+
// would be a guess. Named for what it actually measures.
|
|
457
|
+
fields_with_build_references_redacted: counter.hits,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/** Say what the reported GPU means in the deployment's current lifecycle state. */
|
|
462
|
+
function gpuState(status) {
|
|
463
|
+
switch (status) {
|
|
464
|
+
case "queued_capacity":
|
|
465
|
+
return "not held yet — waiting for stock, nothing charged";
|
|
466
|
+
case "running":
|
|
467
|
+
case "degraded":
|
|
468
|
+
return "held and serving";
|
|
469
|
+
case "provisioning":
|
|
470
|
+
case "downloading_weights":
|
|
471
|
+
case "loading_model":
|
|
472
|
+
return "being booked or booting — not serving yet";
|
|
473
|
+
case "stopped":
|
|
474
|
+
case "paused_insufficient_funds":
|
|
475
|
+
case "failed":
|
|
476
|
+
case "crash_loop":
|
|
477
|
+
case "deleting":
|
|
478
|
+
case "deleted":
|
|
479
|
+
return "not held — this is the SKU the deployment would resume on";
|
|
480
|
+
default:
|
|
481
|
+
return "unknown";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Summarize what a deployment detail payload ACTUALLY proves about the GPU and
|
|
486
|
+
* the lifecycle phase.
|
|
487
|
+
*
|
|
488
|
+
* The detail response is wide and reports several things as null until a pod
|
|
489
|
+
* callback lands, which reads to an agent as "no GPU" and "no phase" on a
|
|
490
|
+
* deployment that is demonstrably provisioning on a known SKU. Everything here
|
|
491
|
+
* is derived from fields the same payload returned — nothing is invented, and a
|
|
492
|
+
* value that cannot be derived stays "unknown" instead of being guessed.
|
|
493
|
+
*/
|
|
494
|
+
export function summarizeInferenceStatus(data) {
|
|
495
|
+
const payload = asRecord(data);
|
|
496
|
+
if (!payload)
|
|
497
|
+
return data;
|
|
498
|
+
const status = nonEmptyString(payload.status) ?? "unknown";
|
|
499
|
+
const gpuType = nonEmptyString(payload.gpu_type);
|
|
500
|
+
const gpuCount = typeof payload.gpu_count === "number" ? payload.gpu_count : undefined;
|
|
501
|
+
const gpu = gpuType ? `${gpuCount && gpuCount > 0 ? gpuCount : 1}x ${gpuType}` : "unknown";
|
|
502
|
+
const download = asRecord(payload.download_progress);
|
|
503
|
+
const boot = asRecord(payload.boot_progress);
|
|
504
|
+
// Phase precedence: the agent's own weight/load report is the most advanced
|
|
505
|
+
// signal, then the pre-agent boot report, then the durable lifecycle status.
|
|
506
|
+
const reportedPhase = nonEmptyString(download?.phase) ?? nonEmptyString(boot?.phase);
|
|
507
|
+
const phaseSource = nonEmptyString(download?.phase)
|
|
508
|
+
? "agent_progress_report"
|
|
509
|
+
: nonEmptyString(boot?.phase)
|
|
510
|
+
? "pre_agent_boot_report"
|
|
511
|
+
: "deployment_status";
|
|
512
|
+
const percent = typeof download?.percent === "number"
|
|
513
|
+
? download.percent
|
|
514
|
+
: typeof boot?.percent === "number" ? boot.percent : null;
|
|
515
|
+
const endpoint = nonEmptyString(payload.endpoint_url);
|
|
516
|
+
const summary = {
|
|
517
|
+
status,
|
|
518
|
+
phase: reportedPhase ?? status,
|
|
519
|
+
phase_source: phaseSource,
|
|
520
|
+
phase_percent: percent,
|
|
521
|
+
gpu,
|
|
522
|
+
gpu_tier: nonEmptyString(payload.gpu_tier) ?? "unknown",
|
|
523
|
+
// What the GPU above MEANS right now: a queued or provisioning deployment
|
|
524
|
+
// does not hold the SKU yet, it is the one being waited/booked for.
|
|
525
|
+
gpu_state: gpuState(status),
|
|
526
|
+
endpoint_usable: status === "running" && Boolean(endpoint),
|
|
527
|
+
desired_state: nonEmptyString(payload.desired_state) ?? "unknown",
|
|
528
|
+
error_code: nonEmptyString(payload.error_code) ?? nonEmptyString(payload.status_reason) ?? null,
|
|
529
|
+
note: "Derived by the MCP server from the fields in this same response; the raw fields above are unchanged. "
|
|
530
|
+
+ "Report the endpoint as live only when endpoint_usable is true.",
|
|
531
|
+
};
|
|
532
|
+
const shaped = { ...payload, status_summary: summary };
|
|
533
|
+
// A present-but-null `gpu`/`phase` reads as "there is none" even when the row
|
|
534
|
+
// proves otherwise. Fill from the derived value and say so, rather than
|
|
535
|
+
// passing a null the agent will misread.
|
|
536
|
+
for (const key of ["gpu", "phase"]) {
|
|
537
|
+
if (key in shaped && shaped[key] === null && summary[key] !== "unknown") {
|
|
538
|
+
shaped[key] = summary[key];
|
|
539
|
+
summary.filled_null_fields = [...(summary.filled_null_fields ?? []), key];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return shaped;
|
|
543
|
+
}
|
|
544
|
+
/* ─── Server setup ─── */
|
|
545
|
+
export function createBiosMcpServer(client, hooks, deploymentCaps,
|
|
546
|
+
// Pre-launch surface gates (src/config.ts). While a gate is on, its creation
|
|
547
|
+
// tools are NOT REGISTERED at all — hidden from tools/list so an agent never
|
|
548
|
+
// learns they exist — and the platform guide stops teaching their workflows.
|
|
549
|
+
// Defaults read the environment so a deployment with nothing set fails CLOSED
|
|
550
|
+
// (gated) while pre-launch; tests that exercise creation pass
|
|
551
|
+
// { training: false, datasets: false }.
|
|
552
|
+
launchGates = resolveLaunchGates()) {
|
|
553
|
+
// `instructions` reaches the client at initialize, BEFORE any tool description
|
|
554
|
+
// is read, which is the only place to state the rules a tool schema cannot: an
|
|
555
|
+
// agent that learns the derived-settings and catalog rules up front never
|
|
556
|
+
// proposes a value the control plane must reject.
|
|
557
|
+
// While a creation surface is gated, the instructions must not present it as
|
|
558
|
+
// available: an agent reads these BEFORE any tool list, and a capability named
|
|
559
|
+
// here is a capability it will try. The gated wording names what is live and
|
|
560
|
+
// states — once, without naming tool names — that creation is not offered.
|
|
561
|
+
const gatedSurfaces = [
|
|
562
|
+
...(launchGates.training ? ["fine-tuning"] : []),
|
|
563
|
+
...(launchGates.datasets ? ["dataset upload/import"] : []),
|
|
564
|
+
];
|
|
565
|
+
const prelaunchNote = gatedSurfaces.length
|
|
566
|
+
? `${gatedSurfaces.join(" and ")} ${gatedSurfaces.length > 1 ? "are" : "is"} not available in this deployment yet — `
|
|
567
|
+
+ "no creation tools for them are offered, so do not attempt to start one or look for a workaround. "
|
|
568
|
+
+ "Existing jobs and datasets stay readable.\n"
|
|
569
|
+
: "";
|
|
570
|
+
const mcp = new McpServer({
|
|
571
|
+
name: "Run BiOS",
|
|
572
|
+
version: VERSION,
|
|
573
|
+
}, {
|
|
574
|
+
instructions: (launchGates.training
|
|
575
|
+
? "Run BiOS serves models on its own GPU control plane. Call get_platform_guide first.\n"
|
|
576
|
+
: "Run BiOS trains and serves models on its own GPU control plane. Call get_platform_guide first.\n")
|
|
577
|
+
+ prelaunchNote
|
|
578
|
+
+ "Rules that no tool schema can express:\n"
|
|
579
|
+
+ (launchGates.training
|
|
580
|
+
? "1. Models: only ids in the Run BiOS catalog can be served (list_models / search_models). "
|
|
581
|
+
: "1. Models: only ids in the Run BiOS catalog can be trained or served (list_models / search_models). ")
|
|
582
|
+
+ "This is not a Hugging Face lookup; an id outside the catalog is rejected, never mirrored on demand.\n"
|
|
583
|
+
+ "2. Serving is platform-derived: serving mode (full/adapter/merged), the OpenAI task "
|
|
584
|
+
+ "(chat, or completion for a base model with no chat template, or embedding/rerank), and image support are "
|
|
585
|
+
+ "derived from the model and checkpoint lineage. They are not tool inputs — read them from the "
|
|
586
|
+
+ "preflight/create/status response.\n"
|
|
587
|
+
+ `3. Context window: the default is min(model native max, ${CONTEXT_DEFAULT_CEILING}) tokens, falling back to `
|
|
588
|
+
+ `${CONTEXT_UNKNOWN_FALLBACK} when the native window is unknown, and the hard ceiling is the model's own native max. `
|
|
589
|
+
+ "Read model.native_max_context from preflight_inference before proposing a value.\n"
|
|
590
|
+
+ "4. GPUs and money: never substitute a GPU or exceed an approved price cap without asking. A capacity "
|
|
591
|
+
+ "rejection arrives as compact JSON with bookable alternatives; a 503 is a transient outage, never an out-of-stock verdict.\n"
|
|
592
|
+
+ "5. Report an endpoint as live only when the status says running.",
|
|
593
|
+
});
|
|
594
|
+
// The registrations below are moved verbatim from the original single-file
|
|
595
|
+
// entry and intentionally keep their original indentation. `server.tool(...)`
|
|
596
|
+
// is a thin delegate that adds the onToolCall hook around each handler.
|
|
597
|
+
const server = {
|
|
598
|
+
tool(name, description, paramsSchema, cb) {
|
|
599
|
+
mcp.tool(name, description, paramsSchema, wrapToolHandler(name, cb, hooks?.onToolCall));
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
603
|
+
/* TOOL: get_platform_guide */
|
|
604
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
605
|
+
server.tool("get_platform_guide", launchGates.training || launchGates.datasets
|
|
606
|
+
? "Get a comprehensive guide to the Run BiOS platform. Call this FIRST to understand the available capabilities, the correct workflow order, supported models, GPU tiers, and best practices for serving models."
|
|
607
|
+
: "Get a comprehensive guide explaining how to use the Run BiOS fine-tuning platform. Call this FIRST to understand the available capabilities, the correct workflow order, supported models, training methods, GPU tiers, dataset formats, and best practices. This gives you all the context needed to help users fine-tune models effectively.", {
|
|
608
|
+
topic: z
|
|
609
|
+
.enum([
|
|
610
|
+
"overview",
|
|
611
|
+
"quick_start",
|
|
612
|
+
"models",
|
|
613
|
+
"datasets",
|
|
614
|
+
"training_methods",
|
|
615
|
+
"gpu_selection",
|
|
616
|
+
"inference",
|
|
617
|
+
"hyperparameters",
|
|
618
|
+
"monitoring",
|
|
619
|
+
"cost_optimization",
|
|
620
|
+
"capacity_errors",
|
|
621
|
+
])
|
|
622
|
+
.optional()
|
|
623
|
+
.describe("Specific topic to learn about. Omit for the full platform overview."),
|
|
624
|
+
}, async ({ topic }) => {
|
|
625
|
+
/* PRE-LAUNCH GATES (src/config.ts): while a creation surface is gated, the
|
|
626
|
+
guide must not teach its workflow — an agent that reads "call
|
|
627
|
+
upload_dataset" will try a tool that is not registered. Gated topics get
|
|
628
|
+
a stand-in that says what launches soon and names only tools that ARE
|
|
629
|
+
registered (reads/lifecycle stay live). At launch the gates open and the
|
|
630
|
+
real topics return verbatim. */
|
|
631
|
+
// 42 tools with every creation tool registered; the gates hide three
|
|
632
|
+
// (create_training_job, upload_dataset, import_huggingface_dataset). The
|
|
633
|
+
// coming-soon gate test pins this claim to the wire count, so drift fails.
|
|
634
|
+
const liveToolCount = 42
|
|
635
|
+
- (launchGates.training ? 1 : 0)
|
|
636
|
+
- (launchGates.datasets ? 2 : 0);
|
|
637
|
+
const comingSoonNote = `# Coming soon
|
|
638
|
+
|
|
639
|
+
${gatedSurfaces.join(" and ")} ${gatedSurfaces.length > 1 ? "are" : "is"} not available in this build yet — the creation tools are not registered, so no tool call (and no workaround) can start one today. Do not retry or search for one.
|
|
640
|
+
|
|
641
|
+
What still works: every read and lifecycle tool for existing datasets and training jobs (list_datasets, preview_dataset, delete_dataset, list_training_jobs, get_training_status, get_training_metrics, get_training_logs, get_training_evals, stop_training_job, resume_training_job, get_training_checkpoints, get_checkpoint_download_manifest, delete_training_checkpoint), and the full inference surface (preflight_inference, create_inference, get_inference_status, chat_with_inference).`;
|
|
642
|
+
const gatedOverview = `# Run BiOS Platform
|
|
643
|
+
|
|
644
|
+
Run BiOS serves models on its own GPU control plane: a curated catalog of verified models — mirrored in its own storage, never fetched live from Hugging Face — deployed to dedicated OpenAI-compatible endpoints.
|
|
645
|
+
|
|
646
|
+
## Core workflow (serving, fully live)
|
|
647
|
+
1. **Find a model** — search_models / list_models read the hosted catalog; these are the only models that can be deployed
|
|
648
|
+
2. **Check GPU fit and price** — get_inference_gpu_options joins model-fit counts to live stock and total hourly prices
|
|
649
|
+
3. **Preflight** — preflight_inference validates the exact request with no wallet, queue, database, or GPU side effects
|
|
650
|
+
4. **Create the deployment** — create_inference books capacity book-before-reveal; a definitive miss returns CAPACITY_UNAVAILABLE and nothing exists
|
|
651
|
+
5. **Run** — poll get_inference_status until status is running; call the endpoint with chat_with_inference
|
|
652
|
+
6. **Control spend** — stop_inference / resume_inference / restart_inference / delete_inference
|
|
653
|
+
|
|
654
|
+
## Launching soon — not offered in this build
|
|
655
|
+
${gatedSurfaces.join(" and ")} ${gatedSurfaces.length > 1 ? "launch" : "launches"} soon: the creation tools are not registered, so no tool call — and no workaround — can start one today. Existing datasets and training jobs are unaffected: every read and lifecycle tool below still works.
|
|
656
|
+
|
|
657
|
+
## What the PLATFORM decides for you (do not try to set these)
|
|
658
|
+
Serving settings are derived from the model itself and are immutable. They are
|
|
659
|
+
NOT tool inputs, and attempting to choose one is rejected:
|
|
660
|
+
- **Serving mode** (full / adapter / merged) comes from the verified checkpoint's
|
|
661
|
+
lineage; a catalog base model is always served whole.
|
|
662
|
+
- **OpenAI task** comes from the resolved model: an instruct/chat model and a
|
|
663
|
+
vision-language model serve as chat, a base model with no chat template serves
|
|
664
|
+
as text completion, and an embedding/reranker architecture serves as
|
|
665
|
+
embeddings/reranking. Every model in the current catalog resolves to chat.
|
|
666
|
+
- **Image input** comes from the model's own config, never from a caller claim.
|
|
667
|
+
- **Context window**: the default is min(model native max, 262144) tokens; a
|
|
668
|
+
model whose native window is unknown falls back to 32768, a model with a
|
|
669
|
+
smaller window keeps its own, and the hard ceiling is always the model's OWN
|
|
670
|
+
native max. You may pass a smaller context_length; a larger one is rejected.
|
|
671
|
+
Read model.native_max_context from preflight_inference before proposing a value.
|
|
672
|
+
Read the actual values back from preflight_inference, create_inference, and
|
|
673
|
+
get_inference_status instead of asserting them.
|
|
674
|
+
|
|
675
|
+
## Billing
|
|
676
|
+
- Per-second GPU billing (NOT hourly) — you only pay for actual compute time
|
|
677
|
+
- Wallet-based prepaid system with auto top-up option
|
|
678
|
+
- New accounts get $10 welcome credit
|
|
679
|
+
|
|
680
|
+
## Authentication
|
|
681
|
+
- API Key (recommended): Use \`X-API-Key: bios-...\` header — org and workspace are resolved automatically
|
|
682
|
+
- JWT token: Use \`Authorization: Bearer <token>\` header with \`X-Org-ID\` and \`X-Workspace-ID\` headers
|
|
683
|
+
- Call introspect_api_key to see your permissions, scopes, and which tools you can use
|
|
684
|
+
|
|
685
|
+
## Available MCP Tools (${liveToolCount} in this build, listed in recommended order)
|
|
686
|
+
1. get_platform_guide - You're reading this! Context for all capabilities
|
|
687
|
+
2. introspect_api_key - Discover your API key's permissions, scopes, and bound workspace
|
|
688
|
+
3. get_wallet_balance - Check your credits before creating a deployment
|
|
689
|
+
4. search_models / list_models - Find a model from the hosted catalog
|
|
690
|
+
5. get_model_config - Recommended settings for a chosen model
|
|
691
|
+
6. list_supported_architectures - Which architectures can be served
|
|
692
|
+
7. get_gpu_pricing / get_recommended_gpu - GPU options and pricing
|
|
693
|
+
8. list_integrations - Connected Hugging Face accounts
|
|
694
|
+
9. list_datasets / preview_dataset / delete_dataset - Read and clean up existing datasets
|
|
695
|
+
10. get_training_capabilities - Read the authoritative training contract
|
|
696
|
+
11. preflight_training_job - Validate a proposed job payload (no side effects)
|
|
697
|
+
12. list_training_jobs / get_training_status - Monitor existing jobs
|
|
698
|
+
13. get_training_metrics / get_training_evals / get_training_logs - Loss curves, benchmarks, logs
|
|
699
|
+
14. stop_training_job / resume_training_job - Control existing jobs
|
|
700
|
+
15. get_training_checkpoints / get_checkpoint_download_manifest / delete_training_checkpoint - Checkpoint access
|
|
701
|
+
|
|
702
|
+
## Inference MCP Tools
|
|
703
|
+
1. get_inference_gpu_options - Read model-fit choices joined to authoritative deployment stock and prices
|
|
704
|
+
2. preflight_inference - Validate the exact request without wallet, queue, database, or GPU side effects
|
|
705
|
+
3. create_inference - Create only after reviewing preflight alternatives, queue consent, and the price cap
|
|
706
|
+
4. get_inference_booking - Poll a book-before-reveal booking handle from a long-running create_inference
|
|
707
|
+
5. list_inferences / get_inference_status - Read durable lifecycle, queue expiry reason, and wallet-authorization state
|
|
708
|
+
6. get_inference_metrics - Read request, token, latency, throughput, and GPU utilization metrics plus a recent time series
|
|
709
|
+
7. get_inference_notifications - Inspect durable email delivery, retries, and dead letters
|
|
710
|
+
8. update_inference_policy - Change explicit queue consent or the maximum accepted hourly price
|
|
711
|
+
9. stop_inference / resume_inference / restart_inference - Control serving lifecycle
|
|
712
|
+
10. chat_with_inference - Call a serverless catalog model by id or a dedicated deployment via the unified /v1 endpoint (optional streaming)
|
|
713
|
+
11. delete_inference - Permanently remove a deployment after verified infrastructure teardown`;
|
|
714
|
+
const guides = {
|
|
715
|
+
overview: `# Run BiOS Fine-Tuning Platform
|
|
716
|
+
|
|
717
|
+
Run BiOS is a cloud fine-tuning platform for large language models. It hosts a curated catalog of verified base models — mirrored in its own storage, never fetched live from Hugging Face — with 15+ training methods and 6 alignment algorithms.
|
|
718
|
+
|
|
719
|
+
## Core Workflow
|
|
720
|
+
1. **Upload a dataset** — JSONL, Parquet, or CSV. The platform auto-validates format, detects columns, and maps fields.
|
|
721
|
+
2. **Choose a base model** — Search the hosted Run BiOS catalog (search_models / list_models); every listed model is verified end-to-end and these are the only models that can be trained or deployed
|
|
722
|
+
3. **Pick a training method** — SFT for instruction tuning, DPO/SimPO/ORPO/CPO/KTO for preference alignment
|
|
723
|
+
4. **Select adapter** — LoRA (fast, efficient), QLoRA (lower VRAM), or Full fine-tune (max quality)
|
|
724
|
+
5. **Configure hyperparameters** — learning rate, batch size, epochs, LoRA rank, etc. Smart defaults provided.
|
|
725
|
+
6. **Choose a GPU** — A100 80GB, H100, A6000 etc. Platform recommends based on model size.
|
|
726
|
+
7. **Launch and monitor** — Real-time loss curves, eval metrics, training logs, checkpoint saving
|
|
727
|
+
8. **Download or deploy** — Get your fine-tuned model weights or merged checkpoints
|
|
728
|
+
|
|
729
|
+
## What the PLATFORM decides for you (do not try to set these)
|
|
730
|
+
Serving settings are derived from the model itself and are immutable. They are
|
|
731
|
+
NOT tool inputs, and attempting to choose one is rejected:
|
|
732
|
+
- **Serving mode** (full / adapter / merged) comes from the verified checkpoint's
|
|
733
|
+
lineage; a catalog base model is always served whole.
|
|
734
|
+
- **OpenAI task** comes from the resolved model: an instruct/chat model and a
|
|
735
|
+
vision-language model serve as chat, a base model with no chat template serves
|
|
736
|
+
as text completion, and an embedding/reranker architecture serves as
|
|
737
|
+
embeddings/reranking. Every model in the current catalog resolves to chat.
|
|
738
|
+
- **Image input** comes from the model's own config, never from a caller claim.
|
|
739
|
+
- **Context window**: the default is min(model native max, 262144) tokens; a
|
|
740
|
+
model whose native window is unknown falls back to 32768, a model with a
|
|
741
|
+
smaller window keeps its own, and the hard ceiling is always the model's OWN
|
|
742
|
+
native max. You may pass a smaller context_length; a larger one is rejected.
|
|
743
|
+
Read model.native_max_context from preflight_inference before proposing a value.
|
|
744
|
+
Read the actual values back from preflight_inference, create_inference, and
|
|
745
|
+
get_inference_status instead of asserting them.
|
|
746
|
+
|
|
747
|
+
## Billing
|
|
748
|
+
- Per-second GPU billing (NOT hourly) — you only pay for actual compute time
|
|
749
|
+
- Wallet-based prepaid system with auto top-up option
|
|
750
|
+
- New accounts get $10 welcome credit
|
|
751
|
+
|
|
752
|
+
## Authentication
|
|
753
|
+
- API Key (recommended): Use \`X-API-Key: bios-...\` header — org and workspace are resolved automatically
|
|
754
|
+
- JWT token: Use \`Authorization: Bearer <token>\` header with \`X-Org-ID\` and \`X-Workspace-ID\` headers
|
|
755
|
+
- Call introspect_api_key to see your permissions, scopes, and which tools you can use
|
|
756
|
+
|
|
757
|
+
## Available MCP Tools (42 in this build, listed in recommended order)
|
|
758
|
+
1. get_platform_guide - You're reading this! Context for all capabilities
|
|
759
|
+
2. introspect_api_key - Discover your API key's permissions, scopes, and bound workspace
|
|
760
|
+
3. get_wallet_balance - Check if you have enough credits before training
|
|
761
|
+
4. search_models / list_models - Find the right base model from the hosted catalog
|
|
762
|
+
5. get_model_config - Get recommended settings for your chosen model
|
|
763
|
+
6. list_supported_architectures - Check which architectures can be trained or served
|
|
764
|
+
7. get_training_capabilities - Read the authoritative training contract; call this before building automated payloads
|
|
765
|
+
8. get_gpu_pricing - See GPU options and pricing
|
|
766
|
+
9. get_recommended_gpu - Get the best GPU for your model + adapter combo
|
|
767
|
+
10. list_integrations - See the Hugging Face accounts connected to your workspace
|
|
768
|
+
11. list_datasets / upload_dataset / import_huggingface_dataset - Prepare training data
|
|
769
|
+
12. preview_dataset - Verify dataset format before training
|
|
770
|
+
13. delete_dataset - Remove a dataset you no longer need
|
|
771
|
+
14. preflight_training_job - Validate the full job before creating it
|
|
772
|
+
15. create_training_job - Launch the fine-tuning job
|
|
773
|
+
16. list_training_jobs / get_training_status - Monitor progress
|
|
774
|
+
17. get_training_metrics - View loss curves and eval results
|
|
775
|
+
18. get_training_evals - Review benchmark scores and quality assessments
|
|
776
|
+
19. get_training_logs - Read training output logs
|
|
777
|
+
20. stop_training_job / resume_training_job - Control running jobs
|
|
778
|
+
21. get_training_checkpoints - Access saved model checkpoints
|
|
779
|
+
22. get_checkpoint_download_manifest - Get short-lived per-file download URLs for a checkpoint
|
|
780
|
+
23. delete_training_checkpoint - Remove a checkpoint you no longer need
|
|
781
|
+
|
|
782
|
+
## Inference MCP Tools
|
|
783
|
+
1. get_inference_gpu_options - Read model-fit choices joined to authoritative deployment stock and prices
|
|
784
|
+
2. preflight_inference - Validate the exact request without wallet, queue, database, or GPU side effects
|
|
785
|
+
3. create_inference - Create only after reviewing preflight alternatives, queue consent, and the price cap
|
|
786
|
+
4. get_inference_booking - Poll a book-before-reveal booking handle from a long-running create_inference
|
|
787
|
+
5. list_inferences / get_inference_status - Read durable lifecycle, queue expiry reason, and wallet-authorization state
|
|
788
|
+
6. get_inference_metrics - Read request, token, latency, throughput, and GPU utilization metrics plus a recent time series
|
|
789
|
+
7. get_inference_notifications - Inspect durable email delivery, retries, and dead letters
|
|
790
|
+
8. update_inference_policy - Change explicit queue consent or the maximum accepted hourly price
|
|
791
|
+
9. stop_inference / resume_inference / restart_inference - Control serving lifecycle
|
|
792
|
+
10. chat_with_inference - Call a serverless catalog model by id or a dedicated deployment via the unified /v1 endpoint (optional streaming)
|
|
793
|
+
11. delete_inference - Permanently remove a deployment after verified infrastructure teardown`,
|
|
794
|
+
quick_start: `# Quick Start Guide
|
|
795
|
+
|
|
796
|
+
## Fastest path to a fine-tuned model:
|
|
797
|
+
|
|
798
|
+
### Step 1: Check your balance
|
|
799
|
+
Call get_wallet_balance to confirm you have credits available.
|
|
800
|
+
|
|
801
|
+
### Step 2: Search for a model
|
|
802
|
+
Call search_models with a query like "llama 8B" or "mistral 7B".
|
|
803
|
+
For beginners: start with meta-llama/Llama-3.1-8B-Instruct (good balance of quality and speed).
|
|
804
|
+
|
|
805
|
+
### Step 3: Prepare your dataset
|
|
806
|
+
Option A: Upload your own file — call upload_dataset with the file path
|
|
807
|
+
Option B: Import from HuggingFace — call import_huggingface_dataset with a repo ID
|
|
808
|
+
|
|
809
|
+
Dataset must have at minimum an "instruction" and "output" column for SFT, or "chosen" and "rejected" for DPO/alignment.
|
|
810
|
+
|
|
811
|
+
### Step 4: Get recommended config
|
|
812
|
+
Call get_model_config with your model name to see recommended GPU, adapter, and hyperparameters.
|
|
813
|
+
|
|
814
|
+
### Step 5: Create the training job
|
|
815
|
+
Call create_training_job with:
|
|
816
|
+
- model: the model name from step 2
|
|
817
|
+
- dataset_id: the ID from step 3
|
|
818
|
+
- method: "sft" for instruction tuning (most common)
|
|
819
|
+
- adapter: "lora" (fastest, recommended for first jobs)
|
|
820
|
+
- Leave other params as defaults unless you have specific needs
|
|
821
|
+
|
|
822
|
+
### Step 6: Monitor progress
|
|
823
|
+
create_training_job books a GPU before returning (~40s). A 'booked' status means the GPU is secured (booked==secured) and training is starting; 'securing' means it is still booking — poll get_training_status until 'booked'/'running'. A booking-time capacity miss returns CAPACITY_UNAVAILABLE and no job is created.
|
|
824
|
+
Call get_training_status periodically. Training typically takes 1-4 hours for small models.
|
|
825
|
+
If a started job loses its pod it rests at 'interrupted' (billing already stopped, not 'failed') — call resume_training_job to continue from the last checkpoint.
|
|
826
|
+
Call get_training_metrics to see loss curves — loss should decrease steadily.
|
|
827
|
+
|
|
828
|
+
### Tips
|
|
829
|
+
- Start with LoRA, not full fine-tune — it's 10x faster and 5x cheaper
|
|
830
|
+
- 3 epochs is usually enough — more can overfit
|
|
831
|
+
- If loss plateaus early, try increasing learning rate slightly
|
|
832
|
+
- Use early stopping if loss increases for several steps`,
|
|
833
|
+
models: `# Model Catalog
|
|
834
|
+
|
|
835
|
+
Run BiOS serves a curated catalog of models hosted in its own storage — every
|
|
836
|
+
listed model is verified end-to-end for training and serving, and these are
|
|
837
|
+
the ONLY models that can be trained or deployed. The catalog is the source of
|
|
838
|
+
truth: call list_models for everything currently hosted, or search_models to
|
|
839
|
+
filter by name, provider, or size. A model that is not in the catalog is not
|
|
840
|
+
hosted yet — ask an admin to mirror it.
|
|
841
|
+
|
|
842
|
+
list_models and search_models read the same registry, so either one answers.
|
|
843
|
+
If BOTH are unavailable, do not guess an id: any id already returned by
|
|
844
|
+
list_inferences or list_training_jobs is known-hosted, the same catalog is on
|
|
845
|
+
the Models page of the console, and reporting the catalog as unavailable is the
|
|
846
|
+
correct answer. An unverified id fails the create gate anyway.
|
|
847
|
+
|
|
848
|
+
Hosted families typically include Llama, Mistral, Qwen, Gemma, and Phi across
|
|
849
|
+
small (1B-4B), medium (7B-13B), and large (30B-70B+) sizes, plus vision-
|
|
850
|
+
language models — but the live catalog always wins over any list.
|
|
851
|
+
|
|
852
|
+
## Choosing a Model
|
|
853
|
+
- For general tasks: a 7B-8B instruct model (good balance of quality and cost)
|
|
854
|
+
- For coding: a code-specialized instruct model from the catalog
|
|
855
|
+
- For multilingual: Qwen models
|
|
856
|
+
- For low-resource: a 1B-4B mini model
|
|
857
|
+
- For maximum quality: a 70B-class model (requires multi-GPU)
|
|
858
|
+
|
|
859
|
+
Use search_models to find hosted models by name or provider.
|
|
860
|
+
Use get_model_config to see what training methods and GPUs work with a specific model.`,
|
|
861
|
+
datasets: `# Dataset Format Guide
|
|
862
|
+
|
|
863
|
+
## Supported Formats
|
|
864
|
+
- **JSONL** (recommended) — One JSON object per line
|
|
865
|
+
- **Parquet** — Columnar format, efficient for large datasets
|
|
866
|
+
- **CSV** — Comma-separated, auto-detected headers
|
|
867
|
+
|
|
868
|
+
## Required Columns by Training Method
|
|
869
|
+
|
|
870
|
+
### SFT (Supervised Fine-Tuning)
|
|
871
|
+
At minimum: "instruction" + "output"
|
|
872
|
+
Recommended: "instruction" + "input" (optional context) + "output"
|
|
873
|
+
Chat format: "conversations" (list of {role, content} objects)
|
|
874
|
+
|
|
875
|
+
Example JSONL:
|
|
876
|
+
{"instruction": "Summarize this text", "input": "The quick brown fox...", "output": "A fox jumped over a dog."}
|
|
877
|
+
{"instruction": "Translate to French", "output": "Bonjour le monde"}
|
|
878
|
+
|
|
879
|
+
### DPO / SimPO / ORPO / CPO / KTO (Alignment)
|
|
880
|
+
Required: "prompt" + "chosen" + "rejected"
|
|
881
|
+
|
|
882
|
+
Example JSONL:
|
|
883
|
+
{"prompt": "Explain quantum computing", "chosen": "Quantum computing uses qubits...", "rejected": "Quantum computing is magic..."}
|
|
884
|
+
|
|
885
|
+
### CPT (Continued Pre-Training)
|
|
886
|
+
Required: "text" column
|
|
887
|
+
|
|
888
|
+
### VLM (Vision-Language)
|
|
889
|
+
Required: "image" (path/URL) + "instruction" + "output"
|
|
890
|
+
|
|
891
|
+
## Best Practices
|
|
892
|
+
- Minimum 100 rows recommended (1,000+ for best results)
|
|
893
|
+
- Maximum 500MB file size
|
|
894
|
+
- Clean, consistent formatting across all rows
|
|
895
|
+
- Remove duplicates and low-quality examples
|
|
896
|
+
- For SFT: diverse instruction types improve generalization
|
|
897
|
+
- For DPO: "chosen" and "rejected" should differ meaningfully
|
|
898
|
+
|
|
899
|
+
## Column Mapping
|
|
900
|
+
The platform auto-detects standard column names. Non-standard names are mapped automatically:
|
|
901
|
+
- question/query/prompt → instruction
|
|
902
|
+
- answer/response/completion → output
|
|
903
|
+
- context/passage → input
|
|
904
|
+
|
|
905
|
+
## INTEGRATIONS AND MULTIPLE HUGGING FACE ACCOUNTS
|
|
906
|
+
Workspaces can connect multiple Hugging Face accounts under Integrations. Each connection has an integration ID and a label. Call list_integrations to see the accounts connected to your workspace.
|
|
907
|
+
|
|
908
|
+
To choose which connected account an import uses:
|
|
909
|
+
- import_huggingface_dataset takes integration_id (required for imports)
|
|
910
|
+
|
|
911
|
+
Raw Hugging Face tokens are never accepted by MCP tools; only stored integration IDs are.
|
|
912
|
+
Rotating a token on the Integrations page heals every dataset linked to that integration automatically.`,
|
|
913
|
+
training_methods: `# Training Methods
|
|
914
|
+
|
|
915
|
+
## SFT — Supervised Fine-Tuning ⭐ Most Common
|
|
916
|
+
Best for: Teaching a model to follow specific instructions or generate in a particular style.
|
|
917
|
+
Input: instruction + output pairs.
|
|
918
|
+
When to use: First fine-tuning job, domain-specific tasks, style transfer, format compliance.
|
|
919
|
+
|
|
920
|
+
## DPO — Direct Preference Optimization
|
|
921
|
+
Best for: Aligning model outputs with human preferences without a reward model.
|
|
922
|
+
Input: prompt + chosen response + rejected response.
|
|
923
|
+
When to use: After SFT, to improve response quality based on preference data.
|
|
924
|
+
|
|
925
|
+
## SimPO — Simple Preference Optimization
|
|
926
|
+
Best for: Simpler alternative to DPO with comparable results.
|
|
927
|
+
Input: Same as DPO (prompt + chosen + rejected).
|
|
928
|
+
When to use: When DPO training is unstable or you want faster convergence.
|
|
929
|
+
|
|
930
|
+
## ORPO — Odds Ratio Preference Optimization
|
|
931
|
+
Best for: Combined SFT + preference alignment in a single training run.
|
|
932
|
+
Input: prompt + chosen + rejected.
|
|
933
|
+
When to use: When you want alignment without a separate SFT step first.
|
|
934
|
+
|
|
935
|
+
## CPO — Contrastive Preference Optimization
|
|
936
|
+
Best for: Strong alignment with contrastive learning signal.
|
|
937
|
+
Input: prompt + chosen + rejected.
|
|
938
|
+
When to use: When you need stronger preference separation than DPO provides.
|
|
939
|
+
|
|
940
|
+
## KTO — Kahneman-Tversky Optimization
|
|
941
|
+
Best for: Alignment with unpaired preference data (just good OR bad, not both).
|
|
942
|
+
Input: prompt + completion + label (good/bad).
|
|
943
|
+
When to use: When you have thumbs-up/thumbs-down data but not paired preferences.
|
|
944
|
+
|
|
945
|
+
## CPT — Continued Pre-Training
|
|
946
|
+
Best for: Injecting domain knowledge into the base model before fine-tuning.
|
|
947
|
+
Input: Raw text documents.
|
|
948
|
+
When to use: Before SFT, when your domain has specialized vocabulary/knowledge.
|
|
949
|
+
|
|
950
|
+
## VLM — Vision-Language Model Training
|
|
951
|
+
Best for: Fine-tuning multimodal models on image+text tasks.
|
|
952
|
+
Input: image + instruction + output.
|
|
953
|
+
When to use: Image captioning, visual QA, document understanding.
|
|
954
|
+
|
|
955
|
+
## Recommended Pipeline
|
|
956
|
+
1. CPT (if domain-specific knowledge needed) → 2. SFT → 3. DPO/SimPO (alignment)
|
|
957
|
+
For most users: SFT alone is sufficient.`,
|
|
958
|
+
gpu_selection: `# GPU Selection Guide
|
|
959
|
+
|
|
960
|
+
## Available GPUs
|
|
961
|
+
|
|
962
|
+
| GPU | VRAM | Best For | Per-Second Cost |
|
|
963
|
+
|-----|------|----------|-----------------|
|
|
964
|
+
| A6000 | 48 GB | Models up to 13B with LoRA | ~$0.0007/sec |
|
|
965
|
+
| A100 80GB | 80 GB | Models up to 70B with QLoRA, up to 13B full | ~$0.0007/sec |
|
|
966
|
+
| H100 | 80 GB | Large models, fastest training | ~$0.0011/sec |
|
|
967
|
+
|
|
968
|
+
## Model Size → GPU Recommendations
|
|
969
|
+
|
|
970
|
+
### LoRA Fine-Tuning (most common)
|
|
971
|
+
- 1B-3B models: A6000 (48GB) — plenty of headroom
|
|
972
|
+
- 7B-13B models: A100 80GB ⭐ sweet spot
|
|
973
|
+
- 30B-70B models: A100 80GB or H100 (may need multi-GPU)
|
|
974
|
+
|
|
975
|
+
### QLoRA Fine-Tuning (4-bit quantized, uses less VRAM)
|
|
976
|
+
- 7B-13B models: A6000 (48GB) — enough with quantization
|
|
977
|
+
- 30B-70B models: A100 80GB
|
|
978
|
+
- 70B+ models: H100 or multi-GPU A100
|
|
979
|
+
|
|
980
|
+
### Full Fine-Tuning (no adapter, maximum quality)
|
|
981
|
+
- 1B-3B models: A100 80GB
|
|
982
|
+
- 7B models: 2-4x A100 80GB
|
|
983
|
+
- 13B+ models: 4-8x A100 or H100
|
|
984
|
+
|
|
985
|
+
## Cost Optimization Tips
|
|
986
|
+
- Run BiOS bills per-SECOND, not per-hour — stop jobs early to save money
|
|
987
|
+
- LoRA training is 5-10x cheaper than full fine-tune for comparable quality
|
|
988
|
+
- QLoRA adds ~10% training time but halves VRAM needs
|
|
989
|
+
- Start with fewer epochs (1-2) to validate, then train full if results look good
|
|
990
|
+
- Use the get_recommended_gpu tool to get the optimal GPU for your specific setup
|
|
991
|
+
|
|
992
|
+
## Multi-GPU
|
|
993
|
+
For models that don't fit in a single GPU's VRAM, the platform automatically shards across GPUs.
|
|
994
|
+
Specify gpu_count in create_training_job (default: 1).
|
|
995
|
+
|
|
996
|
+
## Infrastructure and Price Caps
|
|
997
|
+
The platform selects and manages the underlying infrastructure automatically. You choose only the GPU type, the GPU count, and a maximum total hourly price cap. Preflight returns ranked alternatives and the exact price that will be honored. A price above the approved cap is rejected rather than silently accepted.`,
|
|
998
|
+
inference: `# Model Inference Guide
|
|
999
|
+
|
|
1000
|
+
Deployments serve either a verified fine-tuning checkpoint or a base model from the catalog through an OpenAI-compatible endpoint.
|
|
1001
|
+
|
|
1002
|
+
## Settings the platform derives (not tool inputs)
|
|
1003
|
+
serving_mode, model_task and supports_images are SERVER-DERIVED and immutable,
|
|
1004
|
+
so preflight_inference/create_inference do not accept them:
|
|
1005
|
+
- serving_mode (full / adapter / merged) follows the verified checkpoint's
|
|
1006
|
+
lineage; a catalog base model is served whole.
|
|
1007
|
+
- model_task follows the resolved model — chat for an instruct or
|
|
1008
|
+
vision-language model, completion for a base model with no chat template,
|
|
1009
|
+
embedding/rerank for those architectures. Every model in the current catalog
|
|
1010
|
+
resolves to chat, so there is nothing to choose.
|
|
1011
|
+
- supports_images follows the model's own config.
|
|
1012
|
+
Read all three from the preflight/create response and from get_inference_status.
|
|
1013
|
+
If you copy canonical_request from preflight into create, these fields are
|
|
1014
|
+
ignored rather than honored — the server derives them again either way.
|
|
1015
|
+
|
|
1016
|
+
## Context window
|
|
1017
|
+
- Default: min(model native max, 262144) tokens. A model whose native window is
|
|
1018
|
+
unknown falls back to 32768; a model with a smaller native window keeps it.
|
|
1019
|
+
- Hard ceiling: the model's OWN native max. A larger context_length is rejected
|
|
1020
|
+
with the model's maximum in the message; there is no platform-wide maximum you
|
|
1021
|
+
can rely on, so read model.native_max_context from preflight_inference.
|
|
1022
|
+
- Raising the window enlarges the KV cache and can raise min_gpus, so re-read
|
|
1023
|
+
get_inference_gpu_options after changing it.
|
|
1024
|
+
|
|
1025
|
+
## Safe automation workflow
|
|
1026
|
+
1. Call get_inference_gpu_options with model facts to obtain model-fit counts plus authoritative deployment stock and total hourly prices.
|
|
1027
|
+
2. Choose a concrete GPU type/count. Never infer availability from fit alone and never substitute a different SKU without user approval.
|
|
1028
|
+
3. Call preflight_inference. Review selected_gpu, alternatives, queue_required, billing.authorization_amount_cents, and billing.max_price_hour_cents.
|
|
1029
|
+
4. If the chosen SKU is unavailable, ask the user to choose an available alternative or explicitly consent to allow_capacity_queue. The queue can wait up to seven days.
|
|
1030
|
+
5. Copy canonical_request from preflight—including hf_model_revision/base_model_revision—into create_inference with the approved maximum total hourly price. Store the one-time inference_key securely.
|
|
1031
|
+
6. Poll get_inference_status for durable queue/provisioning/loading/running state. Do not report the endpoint as live until status is running.
|
|
1032
|
+
7. Stop or delete when no longer needed. Resume reauthorizes capacity under the saved price cap.
|
|
1033
|
+
|
|
1034
|
+
## Billing and capacity semantics
|
|
1035
|
+
- Preflight is side-effect free.
|
|
1036
|
+
- Create authorizes two hours at the accepted maximum price without immediately reducing wallet balance.
|
|
1037
|
+
- One hour at the final confirmed price is captured only after capacity is accepted, and a price above the cap is rejected.
|
|
1038
|
+
- Deployment serving supports the secure inventory tier only. Unknown inventory is reported as unknown, never as zero or available.
|
|
1039
|
+
- Training and deployment use the shared capacity broker. It evaluates priority first, then rotates fairly across users at equal priority while preserving FIFO inside each user's own requests. This prevents a high-volume user from starving others; do not describe the queue as strict global FIFO.`,
|
|
1040
|
+
hyperparameters: `# Hyperparameter Guide
|
|
1041
|
+
|
|
1042
|
+
## Key Hyperparameters
|
|
1043
|
+
|
|
1044
|
+
### Learning Rate
|
|
1045
|
+
- Default: 2e-4 (0.0002)
|
|
1046
|
+
- Range: 1e-5 to 5e-4
|
|
1047
|
+
- Higher = faster learning but risk of instability
|
|
1048
|
+
- Lower = more stable but slower convergence
|
|
1049
|
+
- Tip: Start with 2e-4 for LoRA, 2e-5 for full fine-tune
|
|
1050
|
+
|
|
1051
|
+
### Batch Size (per_device_train_batch_size)
|
|
1052
|
+
- Default: 4
|
|
1053
|
+
- Range: 1-32 (limited by GPU VRAM)
|
|
1054
|
+
- Larger = smoother gradients, more stable training
|
|
1055
|
+
- Smaller = faster per-step but noisier
|
|
1056
|
+
- Use gradient_accumulation_steps to simulate larger batches
|
|
1057
|
+
|
|
1058
|
+
### Epochs (num_train_epochs)
|
|
1059
|
+
- Default: 3
|
|
1060
|
+
- Range: 1-10
|
|
1061
|
+
- More epochs = more passes through data
|
|
1062
|
+
- Watch for overfitting: if eval loss starts increasing, you've trained too long
|
|
1063
|
+
- For small datasets (<1000 rows): 3-5 epochs
|
|
1064
|
+
- For large datasets (>10000 rows): 1-3 epochs
|
|
1065
|
+
|
|
1066
|
+
### LoRA Rank (lora_r)
|
|
1067
|
+
- Default: 16
|
|
1068
|
+
- Range: 4-256
|
|
1069
|
+
- Higher = more trainable parameters, better quality, but slower
|
|
1070
|
+
- 8-16 is sufficient for most tasks
|
|
1071
|
+
- 32-64 for complex tasks requiring more model capacity
|
|
1072
|
+
- 128+ for near-full-fine-tune quality
|
|
1073
|
+
|
|
1074
|
+
### LoRA Alpha (lora_alpha)
|
|
1075
|
+
- Default: 32 (typically 2x lora_rank)
|
|
1076
|
+
- Controls the scaling of LoRA updates
|
|
1077
|
+
- Rule of thumb: set to 2x your lora_rank
|
|
1078
|
+
|
|
1079
|
+
### Max Sequence Length
|
|
1080
|
+
- Default: 2048
|
|
1081
|
+
- Range: 512-8192
|
|
1082
|
+
- Must cover your longest training example
|
|
1083
|
+
- Longer = more VRAM usage
|
|
1084
|
+
- Set to slightly above your longest example
|
|
1085
|
+
|
|
1086
|
+
### LR Scheduler
|
|
1087
|
+
- cosine (default) — gradually decreases, good for most tasks
|
|
1088
|
+
- linear — constant decrease, predictable
|
|
1089
|
+
- constant_with_warmup — stays flat after warmup, good for short training
|
|
1090
|
+
|
|
1091
|
+
### Warmup Ratio
|
|
1092
|
+
- Default: 0.03 (3% of total steps)
|
|
1093
|
+
- Prevents early instability by slowly ramping up learning rate
|
|
1094
|
+
|
|
1095
|
+
## Recommended Defaults by Use Case
|
|
1096
|
+
| Parameter | Quick Test | Standard | High Quality |
|
|
1097
|
+
|-----------|-----------|----------|--------------|
|
|
1098
|
+
| Adapter | LoRA | LoRA | Full |
|
|
1099
|
+
| Learning Rate | 3e-4 | 2e-4 | 2e-5 |
|
|
1100
|
+
| Epochs | 1 | 3 | 5 |
|
|
1101
|
+
| Batch Size | 4 | 4 | 8 |
|
|
1102
|
+
| LoRA Rank | 8 | 16 | 64 |
|
|
1103
|
+
| Scheduler | cosine | cosine | cosine |`,
|
|
1104
|
+
monitoring: `# Training Monitoring Guide
|
|
1105
|
+
|
|
1106
|
+
## Key Metrics to Watch
|
|
1107
|
+
|
|
1108
|
+
### Training Loss
|
|
1109
|
+
- Should decrease steadily during training
|
|
1110
|
+
- Sharp drops early are normal (learning basic patterns)
|
|
1111
|
+
- Gradual decrease later shows fine-grained learning
|
|
1112
|
+
- If loss plateaus: try higher learning rate or more LoRA rank
|
|
1113
|
+
- If loss spikes: learning rate is too high, reduce it
|
|
1114
|
+
|
|
1115
|
+
### Eval Loss (if eval dataset provided)
|
|
1116
|
+
- Should track training loss but may be slightly higher
|
|
1117
|
+
- If eval loss increases while training loss decreases = OVERFITTING
|
|
1118
|
+
- Early sign of overfitting: gap between train and eval loss growing
|
|
1119
|
+
- Action: Stop training and use the last checkpoint before divergence
|
|
1120
|
+
|
|
1121
|
+
### Learning Rate
|
|
1122
|
+
- Should follow your chosen schedule (cosine, linear, etc.)
|
|
1123
|
+
- Warmup period: LR starts low and ramps up
|
|
1124
|
+
- Main phase: LR follows the schedule downward
|
|
1125
|
+
|
|
1126
|
+
## Monitoring Tools
|
|
1127
|
+
1. get_training_status — Progress %, current step, elapsed time, ETA
|
|
1128
|
+
2. get_training_metrics — Full loss curve data for analysis
|
|
1129
|
+
3. get_training_logs — Raw training output for debugging
|
|
1130
|
+
4. get_training_checkpoints — Saved model snapshots at intervals
|
|
1131
|
+
|
|
1132
|
+
## When to Stop Early
|
|
1133
|
+
- Loss has plateaued for many steps (no improvement)
|
|
1134
|
+
- Eval loss is increasing (overfitting)
|
|
1135
|
+
- You've reached satisfactory quality and want to save GPU cost
|
|
1136
|
+
- The model is producing good outputs in eval samples
|
|
1137
|
+
|
|
1138
|
+
## Checkpoints
|
|
1139
|
+
- Saved automatically at regular intervals during training
|
|
1140
|
+
- Each checkpoint is a complete model snapshot you can use
|
|
1141
|
+
- If training is interrupted, resume from the last checkpoint
|
|
1142
|
+
- Best practice: compare outputs from different checkpoints to pick the best one`,
|
|
1143
|
+
cost_optimization: `# Cost Optimization Guide
|
|
1144
|
+
|
|
1145
|
+
## Per-Second Billing
|
|
1146
|
+
Run BiOS charges per-SECOND of GPU usage, not per hour. This means:
|
|
1147
|
+
- You only pay for actual compute time
|
|
1148
|
+
- Stopping a job immediately stops billing
|
|
1149
|
+
- No wasted time rounding up to the next hour
|
|
1150
|
+
|
|
1151
|
+
## Cost Estimation
|
|
1152
|
+
Before launching a job, estimate cost:
|
|
1153
|
+
1. Call get_recommended_gpu for your model — it includes a cost estimate
|
|
1154
|
+
2. Rough formula: (dataset_rows / batch_size) × epochs × seconds_per_step × gpu_price_per_second
|
|
1155
|
+
|
|
1156
|
+
## Money-Saving Strategies
|
|
1157
|
+
|
|
1158
|
+
### 1. Start Small
|
|
1159
|
+
- Train for 1 epoch first to validate your setup
|
|
1160
|
+
- Check if the loss is decreasing and outputs look reasonable
|
|
1161
|
+
- Only then train for the full 3-5 epochs
|
|
1162
|
+
|
|
1163
|
+
### 2. Use LoRA
|
|
1164
|
+
- LoRA is 5-10x cheaper than full fine-tune
|
|
1165
|
+
- Quality is comparable for most tasks
|
|
1166
|
+
- QLoRA is even cheaper (uses 4-bit quantization)
|
|
1167
|
+
|
|
1168
|
+
### 3. Optimize Batch Size
|
|
1169
|
+
- Larger batch sizes = fewer total steps = less time
|
|
1170
|
+
- Use gradient accumulation to simulate large batches on limited VRAM
|
|
1171
|
+
|
|
1172
|
+
### 4. Choose the Right GPU
|
|
1173
|
+
- Don't pick H100 for a 7B LoRA job — A100 is plenty and cheaper
|
|
1174
|
+
- Use get_recommended_gpu to find the cost-optimal choice
|
|
1175
|
+
|
|
1176
|
+
### 5. Monitor and Stop Early
|
|
1177
|
+
- Watch the loss curve via get_training_metrics
|
|
1178
|
+
- If loss has plateaued, stop early — more epochs won't help
|
|
1179
|
+
- Check wallet balance with get_wallet_balance before long jobs
|
|
1180
|
+
|
|
1181
|
+
### 6. Check Your Balance First
|
|
1182
|
+
- Call get_wallet_balance before creating a job
|
|
1183
|
+
- Ensure you have enough credits for the estimated training time
|
|
1184
|
+
- Set up auto top-up to avoid interruptions mid-training`,
|
|
1185
|
+
capacity_errors: `# Standard GPU Rejection Errors
|
|
1186
|
+
|
|
1187
|
+
Training and inference speak ONE GPU rejection contract: one body shape, and a
|
|
1188
|
+
code that tells you whether waiting can ever help. This MCP server hands you
|
|
1189
|
+
that body as compact JSON in the tool-error text.
|
|
1190
|
+
|
|
1191
|
+
## Two classes, and the difference matters
|
|
1192
|
+
- CAPACITY_UNAVAILABLE (HTTP 409, reason insufficient_stock) is the ONLY
|
|
1193
|
+
transient one: the GPU is simply not free right now. Retrying later or
|
|
1194
|
+
joining the queue can succeed. Training also echoes the deprecated alias
|
|
1195
|
+
SELECTED_GPU_UNAVAILABLE as legacy_code for one release.
|
|
1196
|
+
- GPU_TYPE_TOO_SMALL (model_too_large), GPU_COUNT_BELOW_MINIMUM
|
|
1197
|
+
(below_model_minimum), GPU_COUNT_INVALID (invalid_gpu_count) and
|
|
1198
|
+
GPU_TYPE_UNSUPPORTED (gpu_unsupported) are HTTP 400 and PERMANENT: the
|
|
1199
|
+
request as submitted can never run on that GPU, whatever frees up. Change
|
|
1200
|
+
gpu_type or gpu_count. Do NOT wait and do NOT queue.
|
|
1201
|
+
|
|
1202
|
+
Read retryable_as_submitted (false = permanent) or queue_offered instead of
|
|
1203
|
+
parsing the message.
|
|
1204
|
+
|
|
1205
|
+
## Body fields
|
|
1206
|
+
- reason: insufficient_stock | model_too_large | below_model_minimum | invalid_gpu_count | gpu_unsupported
|
|
1207
|
+
- selected: the gpu_type/gpu_count/tier that was refused, with its live availability_status/available_count
|
|
1208
|
+
- minimum_requirement: selected_gpu_min + selected_valid_counts for the chosen type, plus the per_type table (min_gpus + valid_counts for every type that fits the model). NEVER submit below min_gpus or outside valid_counts.
|
|
1209
|
+
- available_gpus: ALL currently bookable alternatives, cheapest first, each at its minimum count with valid_counts, available_count and price_hour_cents (available_alternatives is the deprecated alias)
|
|
1210
|
+
- queue_offered: true only for a stock miss. When it is true, you may resubmit with allow_capacity_queue=true to wait for stock, and nothing is charged while waiting. When it is false the queue cannot help this request at all.
|
|
1211
|
+
- queue_eligible: whether THIS request already asked to queue (never true when queue_offered is false)
|
|
1212
|
+
- gpu_priorities_entry: 1-based rank of the gpu_priorities entry that was refused, when the rejection belongs to one
|
|
1213
|
+
- checked_at: when the market snapshot behind the verdict was read
|
|
1214
|
+
|
|
1215
|
+
## How to recover
|
|
1216
|
+
1. Pick an entry from available_gpus and retry the SAME call with its gpu_type and a gpu_count >= its min_gpus (the listed gpu_count is that minimum). With zero alternatives, read minimum_requirement.per_type for the types that fit.
|
|
1217
|
+
2. Only when queue_offered is true: resubmit with allow_capacity_queue=true (explicit user consent required) to auto-deploy when stock frees.
|
|
1218
|
+
3. Never invent a GPU/count; never treat unknown availability as out-of-stock; never re-send an unchanged request that was refused as permanent.
|
|
1219
|
+
|
|
1220
|
+
## Book-before-reveal (inference creates)
|
|
1221
|
+
A non-queued create_inference answers 202 with a booking handle while a
|
|
1222
|
+
GPU is secured on real capacity (30-40s typical). The deployment id and the
|
|
1223
|
+
ONE-TIME inference_key exist only after the booking is confirmed. A definitive
|
|
1224
|
+
miss returns this standard error with FRESH alternatives and NO deployment
|
|
1225
|
+
exists (nothing charged, nothing to clean up). Poll get_inference_booking for
|
|
1226
|
+
long bookings.
|
|
1227
|
+
|
|
1228
|
+
## Transient outages are NOT capacity answers
|
|
1229
|
+
503 ADMISSION_UNAVAILABLE (market/broker unreadable) means retry shortly.
|
|
1230
|
+
Never conclude out-of-stock from it.
|
|
1231
|
+
|
|
1232
|
+
## After start
|
|
1233
|
+
Once a deployment has ever been booked, losing its GPU NEVER ends in a
|
|
1234
|
+
capacity failure: the platform replaces it across the ranked backup ladder and,
|
|
1235
|
+
when no stock exists anywhere, parks it as queued_capacity (waiting, zero
|
|
1236
|
+
charge) until stock returns. gpu_priorities backups may be ranked on ANY
|
|
1237
|
+
create (1-5 choices), and the queue itself stays opt-in.`,
|
|
1238
|
+
};
|
|
1239
|
+
// The overview and quick_start teach both creation workflows, so either
|
|
1240
|
+
// gate swaps them out; the single-surface topics follow their own gate.
|
|
1241
|
+
if (launchGates.training || launchGates.datasets) {
|
|
1242
|
+
guides.overview = gatedOverview;
|
|
1243
|
+
guides.quick_start = comingSoonNote;
|
|
1244
|
+
}
|
|
1245
|
+
if (launchGates.training) {
|
|
1246
|
+
guides.training_methods = comingSoonNote;
|
|
1247
|
+
guides.gpu_selection = comingSoonNote;
|
|
1248
|
+
guides.hyperparameters = comingSoonNote;
|
|
1249
|
+
guides.cost_optimization = comingSoonNote;
|
|
1250
|
+
}
|
|
1251
|
+
if (launchGates.datasets) {
|
|
1252
|
+
guides.datasets = comingSoonNote;
|
|
1253
|
+
}
|
|
1254
|
+
if (topic && guides[topic]) {
|
|
1255
|
+
return result(guides[topic]);
|
|
1256
|
+
}
|
|
1257
|
+
return result(guides.overview);
|
|
1258
|
+
});
|
|
1259
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1260
|
+
/* TOOL: introspect_api_key */
|
|
1261
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1262
|
+
server.tool("introspect_api_key", "Discover what this API key can do. Returns the key's permissions (scopes), the org and workspace it's bound to, allowed MCP tools and SDK methods, rate limits, and expiration. Call this first to understand your access level and available capabilities.", {}, async () => {
|
|
1263
|
+
const data = await client.api("/api/api-keys/introspect");
|
|
1264
|
+
return json(data);
|
|
1265
|
+
});
|
|
1266
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1267
|
+
/* TOOL: get_wallet_balance */
|
|
1268
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1269
|
+
server.tool("get_wallet_balance", "Check your current wallet balance, available credits, and pending charges. Call this before creating a training job to make sure you have enough credits. Returns available balance in dollars, pending charges from running jobs, and auto top-up status.", {}, async () => {
|
|
1270
|
+
const data = await client.api("/api/billing/wallet");
|
|
1271
|
+
return json(data);
|
|
1272
|
+
});
|
|
1273
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1274
|
+
/* TOOL: search_models */
|
|
1275
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1276
|
+
server.tool("search_models", launchGates.training
|
|
1277
|
+
? "Search the Run BiOS model catalog — the platform's own hosted, verified models. These are the ONLY models that can be deployed on Run BiOS; a model that is not listed is not hosted yet (ask an admin to mirror it). Filter by name, provider, or parameter count. Returns model id, author, parameter counts, architecture, and downloads/likes for each result."
|
|
1278
|
+
: "Search the Run BiOS model catalog — the platform's own hosted, verified models. These are the ONLY models that can be fine-tuned or deployed on Run BiOS; a model that is not listed is not hosted yet (ask an admin to mirror it). Filter by name, provider, or parameter count. Returns model id, author, parameter counts, architecture, and downloads/likes for each result.", {
|
|
1279
|
+
query: z.string().optional().describe("Search by model name (e.g., 'llama', 'mistral', 'phi', 'qwen')"),
|
|
1280
|
+
provider: z.string().optional().describe("Filter by provider/author (e.g., 'meta-llama', 'mistralai', 'microsoft', 'Qwen')"),
|
|
1281
|
+
max_params: z.string().optional().describe("Maximum parameter count (e.g., '7B', '13B', '70B')"),
|
|
1282
|
+
}, async ({ query, provider, max_params }) => {
|
|
1283
|
+
// The registry matches query tokens against normalized repo ids
|
|
1284
|
+
// ("author/name"), so folding the provider into q turns it into a real
|
|
1285
|
+
// author filter instead of a silently ignored parameter.
|
|
1286
|
+
const q = [provider, query].filter(Boolean).join(" ").trim() || undefined;
|
|
1287
|
+
const data = await client.api("/api/public/model-search", {
|
|
1288
|
+
params: { q, max_params },
|
|
1289
|
+
});
|
|
1290
|
+
return json(data);
|
|
1291
|
+
});
|
|
1292
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1293
|
+
/* TOOL: list_models */
|
|
1294
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1295
|
+
server.tool("list_models", launchGates.training
|
|
1296
|
+
? "List every model hosted on Run BiOS — the platform's own verified registry, mirrored in Run BiOS storage (never a live Hugging Face listing). These are the only models that can be deployed. Call this before create_inference to pick a valid model id; use search_models to filter the same catalog by text."
|
|
1297
|
+
: "List every model hosted on Run BiOS — the platform's own verified registry, mirrored in Run BiOS storage (never a live Hugging Face listing). These are the only models that can be fine-tuned or deployed. Call this before create_training_job or create_inference to pick a valid model id; use search_models to filter the same catalog by text.", {
|
|
1298
|
+
type: z.enum(["all", "llm", "vlm"]).optional().describe("Filter by model surface (default: all)."),
|
|
1299
|
+
sort: z.enum(["downloads", "likes", "trending", "name"]).optional().describe("Sort order (default: downloads)."),
|
|
1300
|
+
limit: z.number().int().min(1).max(60).optional().describe("Page size (default 24, max 60)."),
|
|
1301
|
+
offset: z.number().int().min(0).optional().describe("Pagination offset (default 0)."),
|
|
1302
|
+
}, async ({ type, sort, limit, offset }) => {
|
|
1303
|
+
const data = await client.api("/api/public/model-search", {
|
|
1304
|
+
params: {
|
|
1305
|
+
type: type && type !== "all" ? type : undefined,
|
|
1306
|
+
sort,
|
|
1307
|
+
limit: limit !== undefined ? String(limit) : undefined,
|
|
1308
|
+
offset: offset !== undefined ? String(offset) : undefined,
|
|
1309
|
+
},
|
|
1310
|
+
});
|
|
1311
|
+
return json(data);
|
|
1312
|
+
});
|
|
1313
|
+
/**
|
|
1314
|
+
* Registry gate for model selection: Run BiOS can only train and serve models it
|
|
1315
|
+
* hosts in its own registry. A definitive registry miss gets a clear,
|
|
1316
|
+
* actionable error pointing at the catalog; any failure to ANSWER (registry
|
|
1317
|
+
* unreachable) stays advisory — the server-side gate re-validates
|
|
1318
|
+
* authoritatively and unknown never fails closed.
|
|
1319
|
+
*/
|
|
1320
|
+
async function assertModelHosted(modelId, tool) {
|
|
1321
|
+
const status = await client.modelRegistryStatus(modelId);
|
|
1322
|
+
if (status === "not_hosted") {
|
|
1323
|
+
// Same compact-JSON convention as the capacity errors: an MCP client is
|
|
1324
|
+
// an AI agent and can act on the instruction directly. The instruction
|
|
1325
|
+
// names every route to a valid id (see CATALOG_RECOVERY) rather than a
|
|
1326
|
+
// single tool, so the advice still works when that tool is degraded.
|
|
1327
|
+
throw new Error(JSON.stringify({
|
|
1328
|
+
code: "MODEL_NOT_HOSTED",
|
|
1329
|
+
message: `"${modelId}" is not in the Run BiOS model catalog, so it cannot be trained or served. Run BiOS only runs models it hosts and mirrors itself; ask an admin to mirror this one if you need it.`,
|
|
1330
|
+
recoverable: true,
|
|
1331
|
+
instruction: `${CATALOG_RECOVERY} Then retry ${tool} with that id.`,
|
|
1332
|
+
}));
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Rewrite a model-resolution failure into the answer that is both TRUE and
|
|
1337
|
+
* actionable. The control plane surfaces its mirror's transport error verbatim
|
|
1338
|
+
* ("Could not resolve the model for sizing: Hugging Face request returned
|
|
1339
|
+
* 404"), which names an upstream vendor the caller cannot act on and never
|
|
1340
|
+
* states the real answer: that id is not a Run BiOS catalog model. Callers here
|
|
1341
|
+
* have already passed the registry gate, so a definitive catalog miss is
|
|
1342
|
+
* impossible at this point — the remaining causes are a bad revision or a
|
|
1343
|
+
* catalog entry whose mirrored facts cannot be read right now. Anything that is
|
|
1344
|
+
* not a resolution failure (capacity JSON, auth, validation) is returned
|
|
1345
|
+
* untouched.
|
|
1346
|
+
*/
|
|
1347
|
+
function modelResolveFailure(error, modelId, tool) {
|
|
1348
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1349
|
+
const isResolveFailure = /hugging\s*face/i.test(text)
|
|
1350
|
+
|| /could not resolve (the model|an immutable)/i.test(text)
|
|
1351
|
+
|| /could not establish .*(immutable|commit)/i.test(text);
|
|
1352
|
+
if (!isResolveFailure)
|
|
1353
|
+
return error instanceof Error ? error : new Error(text);
|
|
1354
|
+
const subject = modelId ? `"${modelId}"` : "the requested model";
|
|
1355
|
+
if (/revision|commit/i.test(text)) {
|
|
1356
|
+
return new Error(JSON.stringify({
|
|
1357
|
+
code: "MODEL_REVISION_UNRESOLVED",
|
|
1358
|
+
message: `Run BiOS could not pin an exact, immutable commit for ${subject}, so it refused to size or deploy it. The revision you supplied is not one it can verify.`,
|
|
1359
|
+
recoverable: true,
|
|
1360
|
+
instruction: `Omit the revision to let the platform pin the catalog's current commit, or copy hf_model_revision/base_model_revision unchanged from preflight_inference's canonical_request. Then retry ${tool}.`,
|
|
1361
|
+
model: modelId,
|
|
1362
|
+
}));
|
|
1363
|
+
}
|
|
1364
|
+
return new Error(JSON.stringify({
|
|
1365
|
+
code: "MODEL_NOT_RESOLVED",
|
|
1366
|
+
message: `Run BiOS could not resolve ${subject} from its own model catalog, so it cannot be sized or deployed. A model that is not in the catalog can never resolve; a catalog model can also fail this way while its mirrored facts are being re-synced.`,
|
|
1367
|
+
recoverable: true,
|
|
1368
|
+
instruction: `${CATALOG_RECOVERY} Then retry ${tool}. If the id IS listed in the catalog, report a catalog sync problem to the user or an admin instead of retrying in a loop.`,
|
|
1369
|
+
model: modelId,
|
|
1370
|
+
}));
|
|
1371
|
+
}
|
|
1372
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1373
|
+
/* TOOL: get_model_config */
|
|
1374
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1375
|
+
server.tool("get_model_config", launchGates.training
|
|
1376
|
+
? "Get configuration recommendations for a specific model hosted on Run BiOS (see list_models). Returns supported training methods, recommended GPU type and count, default hyperparameters, minimum VRAM required, and estimated cost per hour."
|
|
1377
|
+
: "Get training configuration recommendations for a specific model hosted on Run BiOS (see list_models). Returns supported training methods, recommended GPU type and count, default hyperparameters, minimum VRAM required, and estimated cost per hour. Use this after search_models to plan your training job.", {
|
|
1378
|
+
model: z.string().describe("Full model id from the Run BiOS catalog (e.g., 'meta-llama/Llama-3.1-8B-Instruct')"),
|
|
1379
|
+
model_revision: z
|
|
1380
|
+
.string()
|
|
1381
|
+
.max(256)
|
|
1382
|
+
.optional()
|
|
1383
|
+
.describe("Model branch, tag, or commit. The response returns the exact immutable commit used for sizing."),
|
|
1384
|
+
integration_id: z
|
|
1385
|
+
.string()
|
|
1386
|
+
.optional()
|
|
1387
|
+
.describe("Legacy no-op for catalog models — hosted models are never gated. Integrations are used for Hugging Face dataset imports."),
|
|
1388
|
+
}, async ({ model }) => {
|
|
1389
|
+
const data = await client.api("/api/public/model-config", { params: { id: model } });
|
|
1390
|
+
return json(data);
|
|
1391
|
+
});
|
|
1392
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1393
|
+
/* TOOL: list_supported_architectures */
|
|
1394
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1395
|
+
server.tool("list_supported_architectures", "List the architectures Run BiOS supports, so you know what can be trained or served before you try. scope='inference' returns the model architecture classes (e.g. LlamaForCausalLM) that can be DEPLOYED for serving; scope='training' returns the architecture keys the fine-tuning gate accepts. If count is 0 the registry is empty and nothing is explicitly restricted (every architecture Run BiOS supports is allowed). Support here is authoritative: the deployment and training gates read these same rows. The response carries NO build or version identity of any kind — read manifest_provenance, which says how many rows came from a published manifest without naming any build. Never report a platform version from this tool. No authentication required.", {
|
|
1396
|
+
scope: z.enum(["inference", "training"]).optional().describe("Which registry to read. Defaults to 'inference' (servable/deployable architectures)."),
|
|
1397
|
+
}, async ({ scope }) => {
|
|
1398
|
+
const data = await client.api("/api/public/serving-architectures", { params: { scope: scope || "inference" } });
|
|
1399
|
+
// The registry TABLE holds the build identity of the engine image whose
|
|
1400
|
+
// capability manifest published each row (image_tag/bios_version/
|
|
1401
|
+
// manifest_sha256, and the same tag again inside free-text `notes`). None
|
|
1402
|
+
// of it is a customer fact and it must never leave the platform, so the
|
|
1403
|
+
// endpoint now projects those columns away before answering.
|
|
1404
|
+
//
|
|
1405
|
+
// Both passes still run, in this order, and neither is redundant — they
|
|
1406
|
+
// defend against the projection regressing, which is the exact way this
|
|
1407
|
+
// leak reached production the first time:
|
|
1408
|
+
// 1. annotateArchitectureProvenance COUNTS how many rows came from a
|
|
1409
|
+
// manifest (from `source`, which survives the projection) and states
|
|
1410
|
+
// what the caller may conclude, so an agent cannot read provenance as
|
|
1411
|
+
// "the version the platform runs now". It goes FIRST because it needs
|
|
1412
|
+
// the rows before step 2 rewrites them.
|
|
1413
|
+
// 2. sanitizeArchitectureRegistry then DROPS every build-identity key at
|
|
1414
|
+
// any depth and content-redacts what is left, so anything the
|
|
1415
|
+
// projection ever fails to strip still never ships.
|
|
1416
|
+
return json(sanitizeArchitectureRegistry(annotateArchitectureProvenance(data)));
|
|
1417
|
+
});
|
|
1418
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1419
|
+
/* TOOL: get_gpu_pricing */
|
|
1420
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1421
|
+
server.tool("get_gpu_pricing", "Get current GPU pricing and availability. Returns all available GPU types with display name, VRAM in GB, per-second cost, availability status, and supported model sizes. No authentication required.", {}, async () => {
|
|
1422
|
+
const data = await client.api("/api/public/gpu-pricing");
|
|
1423
|
+
return json(data);
|
|
1424
|
+
});
|
|
1425
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1426
|
+
/* TOOL: get_recommended_gpu */
|
|
1427
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1428
|
+
server.tool("get_recommended_gpu", "Get authoritative, model-aware GPU options from the training control plane. Returns required counts, live availability, total hourly prices, the cheapest currently bookable recommendation, and compatible alternatives when no GPU is available. It does not invent a duration or total-cost estimate.", {
|
|
1429
|
+
model: z.string().describe("Full model name (e.g., 'meta-llama/Llama-3.1-8B-Instruct')"),
|
|
1430
|
+
adapter: z.enum(TRAINING_ADAPTERS).optional().describe("Adapter type (default: 'lora')."),
|
|
1431
|
+
method: z.enum(["sft", "pt", "rlhf"]).optional().describe("Canonical training method."),
|
|
1432
|
+
rlhf_type: z
|
|
1433
|
+
.enum(["dpo", "simpo", "orpo", "cpo", "kto", "rm"])
|
|
1434
|
+
.optional()
|
|
1435
|
+
.describe("RLHF algorithm when method is rlhf."),
|
|
1436
|
+
model_params_b: z.number().positive().optional().describe("Optional total-parameter hint in billions."),
|
|
1437
|
+
model_active_params_b: z
|
|
1438
|
+
.number()
|
|
1439
|
+
.positive()
|
|
1440
|
+
.optional()
|
|
1441
|
+
.describe("Optional active-parameter hint for MoE models in billions."),
|
|
1442
|
+
}, async (params) => {
|
|
1443
|
+
const data = await client.api("/api/training/gpu-options", {
|
|
1444
|
+
params: buildGPUOptionsParams(params),
|
|
1445
|
+
});
|
|
1446
|
+
return json(data);
|
|
1447
|
+
});
|
|
1448
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1449
|
+
/* TOOL: get_inference_gpu_options */
|
|
1450
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1451
|
+
server.tool("get_inference_gpu_options", "Get inference model-fit GPU choices joined to the authoritative deployment inventory and pricing snapshot. PREFER model-addressed sizing: pass model=<model_id> (optionally revision / hf_integration_id) and the SERVER resolves the facts (vision-aware) and returns computed min_gpus, valid_counts, and bookable_counts — the exact minimums the create gate enforces. Returns valid tensor-parallel counts, exact total hourly prices, availability as available/out_of_stock/unknown, and ranked alternatives. Never interpret unknown inventory as available, never offer a count below min_gpus or outside bookable_counts, and never substitute a SKU without user approval.", {
|
|
1452
|
+
model: z.string().optional().describe("Model id for SERVER-resolved sizing (recommended); replaces every client-fact field below."),
|
|
1453
|
+
revision: z.string().optional().describe("Exact model commit for model-addressed sizing."),
|
|
1454
|
+
inference_id: z.string().optional().describe("Size from an existing deployment's pinned facts."),
|
|
1455
|
+
hf_integration_id: z.string().optional().describe("Legacy field kept for compatibility — catalog models are pre-mirrored and never gated, so no integration is needed."),
|
|
1456
|
+
params_b: z.number().positive().optional().describe("DEPRECATED client-fact path: total model parameters in billions; required only when model/inference_id are absent."),
|
|
1457
|
+
active_params_b: z.number().positive().optional().describe("Active MoE parameters in billions, used only for throughput hints."),
|
|
1458
|
+
is_moe: z.boolean().optional(),
|
|
1459
|
+
is_multimodal: z.boolean().optional().describe("Vision tower present (client-fact path only): reserves extra VRAM and can raise minimums. Model-addressed sizing detects this automatically."),
|
|
1460
|
+
quant: quantSchema("Precision to size the weights and KV cache for."),
|
|
1461
|
+
context_length: contextLengthSchema("Context window to size the KV cache for."),
|
|
1462
|
+
kv_heads: z.number().int().positive().optional(),
|
|
1463
|
+
num_layers: z.number().int().positive().optional(),
|
|
1464
|
+
kv_layers: z.number().int().positive().optional(),
|
|
1465
|
+
head_dim: z.number().int().positive().optional(),
|
|
1466
|
+
attention: z.enum(["gqa", "mla"]).optional(),
|
|
1467
|
+
num_attention_heads: z.number().int().positive().optional(),
|
|
1468
|
+
kv_lora_rank: z.number().int().positive().optional(),
|
|
1469
|
+
qk_rope_head_dim: z.number().int().positive().optional(),
|
|
1470
|
+
gpu_tier: z.enum(DEPLOYMENT_GPU_TIERS).optional(),
|
|
1471
|
+
}, async (params) => {
|
|
1472
|
+
// Gate the model against the Run BiOS catalog FIRST. Without it the control
|
|
1473
|
+
// plane answers a sizing miss with its upstream mirror's 404 text, which
|
|
1474
|
+
// names a vendor and hides the real answer ("that id is not in the Run BiOS
|
|
1475
|
+
// catalog"). A registry that cannot answer stays advisory, exactly as on
|
|
1476
|
+
// the create path. This endpoint also accepts the "repo@revision" form, so
|
|
1477
|
+
// probe the repo id alone — the revision is not part of catalog identity.
|
|
1478
|
+
const catalogId = params.model?.split("@")[0]?.trim();
|
|
1479
|
+
if (catalogId)
|
|
1480
|
+
await assertModelHosted(catalogId, "get_inference_gpu_options");
|
|
1481
|
+
try {
|
|
1482
|
+
const data = await client.api("/api/inference/gpu-options", {
|
|
1483
|
+
params: buildDeploymentGPUOptionsParams(params),
|
|
1484
|
+
});
|
|
1485
|
+
return json(data);
|
|
1486
|
+
}
|
|
1487
|
+
catch (error) {
|
|
1488
|
+
throw modelResolveFailure(error, params.model, "get_inference_gpu_options");
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1492
|
+
/* TOOL: preflight_inference */
|
|
1493
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1494
|
+
server.tool("preflight_inference", "Validate and canonicalize a deployment without creating a database row, authorizing funds, entering a queue, or allocating a GPU. Returns verified model facts, selected price/stock, compatible alternatives, queue requirements, a request hash, and two-hour wallet-authorization terms. The response is also where you READ the derived serving settings: serving_mode, model_task, supports_images and the resolved context_length are chosen by the platform from the model and checkpoint lineage and cannot be set as inputs. Review this result with the user before create_inference.", deploymentCreateToolSchema, async (params) => {
|
|
1495
|
+
if (params.source_type === "hf_model" && params.hf_model_id) {
|
|
1496
|
+
await assertModelHosted(params.hf_model_id, "preflight_inference");
|
|
1497
|
+
}
|
|
1498
|
+
try {
|
|
1499
|
+
const data = await client.api("/api/inference/preflight", {
|
|
1500
|
+
method: "POST",
|
|
1501
|
+
body: buildDeploymentBody(params),
|
|
1502
|
+
});
|
|
1503
|
+
return json(data);
|
|
1504
|
+
}
|
|
1505
|
+
catch (error) {
|
|
1506
|
+
throw modelResolveFailure(error, params.hf_model_id ?? params.base_model_id, "preflight_inference");
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1510
|
+
/* TOOL: create_inference */
|
|
1511
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1512
|
+
/**
|
|
1513
|
+
* Render a GPU type at a count readably: the type name already says GPU, so
|
|
1514
|
+
* only the plural marker is added ("1 H100_80GB" vs "2 H100_80GB GPUs"). Keeps
|
|
1515
|
+
* the synthesized copy identical to the server's.
|
|
1516
|
+
*/
|
|
1517
|
+
const gpusOfType = (gpuType, count) => (count === 1 ? gpuType : `${gpuType} GPUs`);
|
|
1518
|
+
/**
|
|
1519
|
+
* Self-preflight (book-first §2): validate the chosen gpu_type/gpu_count
|
|
1520
|
+
* against the SERVER's model-addressed sizing (computed min_gpus/valid_counts)
|
|
1521
|
+
* BEFORE any create POST, and synthesize the standard structured GPU rejection
|
|
1522
|
+
* when the selection can never be booked. Every rejection this raises is
|
|
1523
|
+
* permanent (GPU_TYPE_TOO_SMALL / GPU_COUNT_BELOW_MINIMUM / GPU_COUNT_INVALID),
|
|
1524
|
+
* never CAPACITY_UNAVAILABLE: the selection is unbookable whatever the stock is.
|
|
1525
|
+
* Advisory only: a failure to ANSWER (endpoint unreachable) never blocks, the
|
|
1526
|
+
* create gate re-validates authoritatively and unknown never fails closed.
|
|
1527
|
+
*/
|
|
1528
|
+
async function assertInferenceGpuSelectionBookable(body) {
|
|
1529
|
+
const model = (body.hf_model_id || body.base_model_id);
|
|
1530
|
+
const gpuType = body.gpu_type;
|
|
1531
|
+
const gpuCount = body.gpu_count;
|
|
1532
|
+
if (!model || !gpuType || !Number.isInteger(gpuCount))
|
|
1533
|
+
return;
|
|
1534
|
+
let options;
|
|
1535
|
+
try {
|
|
1536
|
+
options = await client.api("/api/inference/gpu-options", {
|
|
1537
|
+
params: {
|
|
1538
|
+
model: String(model),
|
|
1539
|
+
revision: (body.hf_model_revision || body.base_model_revision),
|
|
1540
|
+
quant: body.quant,
|
|
1541
|
+
context_length: body.context_length !== undefined ? String(body.context_length) : undefined,
|
|
1542
|
+
hf_integration_id: body.hf_integration_id,
|
|
1543
|
+
},
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
catch {
|
|
1547
|
+
return; // advisory only — the create gate is the enforcement floor
|
|
1548
|
+
}
|
|
1549
|
+
const chosen = options?.options?.find((option) => option?.gpu_type === gpuType);
|
|
1550
|
+
if (!chosen)
|
|
1551
|
+
return;
|
|
1552
|
+
const minGpus = chosen.min_gpus || 0;
|
|
1553
|
+
const validCounts = (chosen.valid_counts || []).filter((count) => Number.isInteger(count));
|
|
1554
|
+
let reason;
|
|
1555
|
+
let message = "";
|
|
1556
|
+
if (chosen.selectable === false) {
|
|
1557
|
+
reason = "model_too_large";
|
|
1558
|
+
message = `This model does not fit on ${gpuType}, at any GPU count.`;
|
|
1559
|
+
}
|
|
1560
|
+
else if (minGpus > 0 && gpuCount < minGpus) {
|
|
1561
|
+
reason = "below_model_minimum";
|
|
1562
|
+
message = `This model needs at least ${minGpus} ${gpusOfType(gpuType, minGpus)} to run, and this request asked for ${gpuCount}.`;
|
|
1563
|
+
}
|
|
1564
|
+
else if (validCounts.length > 0 && !validCounts.includes(gpuCount)) {
|
|
1565
|
+
reason = "invalid_gpu_count";
|
|
1566
|
+
message = `This model cannot be split across ${gpuCount} ${gpusOfType(gpuType, gpuCount)}. Use one of these GPU counts instead: ${validCounts.join(", ")}.`;
|
|
1567
|
+
}
|
|
1568
|
+
if (!reason)
|
|
1569
|
+
return;
|
|
1570
|
+
const selectable = (options.options || []).filter((option) => option && option.selectable !== false);
|
|
1571
|
+
const alternatives = selectable
|
|
1572
|
+
.filter((option) => option.market?.availability_status === "available")
|
|
1573
|
+
.map((option) => ({
|
|
1574
|
+
gpu_type: option.gpu_type,
|
|
1575
|
+
gpu_count: option.min_gpus,
|
|
1576
|
+
min_gpus: option.min_gpus,
|
|
1577
|
+
valid_counts: option.valid_counts,
|
|
1578
|
+
tier: "secure",
|
|
1579
|
+
available_count: option.market?.available_count ?? undefined,
|
|
1580
|
+
price_hour_cents: (option.market?.price_per_gpu_hour_cents || 0) * (option.min_gpus || 1),
|
|
1581
|
+
}));
|
|
1582
|
+
// Same structured shape, codes and instruction text the transport serializes
|
|
1583
|
+
// for real API GPU rejections, so agent clients handle exactly one contract.
|
|
1584
|
+
// Every reason this pre-check can raise is a permanent fact about the request
|
|
1585
|
+
// (the model does not fit, the count is below the model's minimum, the count
|
|
1586
|
+
// cannot be used), so the code is never CAPACITY_UNAVAILABLE and the queue is
|
|
1587
|
+
// never offered: waiting for stock could not book any of them.
|
|
1588
|
+
const permanent = isPermanentGpuCode(gpuRejectionCodeFor(reason));
|
|
1589
|
+
throw new Error(JSON.stringify({
|
|
1590
|
+
code: gpuRejectionCodeFor(reason),
|
|
1591
|
+
status: gpuRejectionStatusFor(reason),
|
|
1592
|
+
reason,
|
|
1593
|
+
message,
|
|
1594
|
+
recoverable: true,
|
|
1595
|
+
retryable_as_submitted: !permanent,
|
|
1596
|
+
instruction: gpuRejectionInstruction({
|
|
1597
|
+
permanent,
|
|
1598
|
+
hasAlternatives: alternatives.length > 0,
|
|
1599
|
+
call: "create_inference",
|
|
1600
|
+
}),
|
|
1601
|
+
available_gpus: alternatives,
|
|
1602
|
+
minimum_requirement: {
|
|
1603
|
+
selected_gpu_min: minGpus,
|
|
1604
|
+
selected_valid_counts: validCounts,
|
|
1605
|
+
per_type: selectable
|
|
1606
|
+
.filter((option) => (option.min_gpus || 0) >= 1)
|
|
1607
|
+
.map((option) => ({
|
|
1608
|
+
gpu_type: option.gpu_type,
|
|
1609
|
+
min_gpus: option.min_gpus,
|
|
1610
|
+
valid_counts: option.valid_counts || [],
|
|
1611
|
+
})),
|
|
1612
|
+
},
|
|
1613
|
+
selected: { gpu_type: gpuType, gpu_count: gpuCount, tier: body.gpu_tier || "secure", availability_status: "unknown" },
|
|
1614
|
+
// A permanent rejection must not advertise the queue, and must not report
|
|
1615
|
+
// itself eligible for it either: a caller branching on queue_eligible would
|
|
1616
|
+
// park a request that can never book.
|
|
1617
|
+
queue_offered: !permanent,
|
|
1618
|
+
queue_eligible: body.allow_capacity_queue === true && !permanent,
|
|
1619
|
+
checked_at: options.availability_checked_at,
|
|
1620
|
+
}));
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Book-before-reveal (book-first §1): poll a 202 booking handle to its
|
|
1624
|
+
* terminal outcome. Pending keeps polling; a transient 503/market outage
|
|
1625
|
+
* keeps polling (never a capacity verdict); the definitive miss throws the
|
|
1626
|
+
* structured CAPACITY_UNAVAILABLE the transport already serialized; on
|
|
1627
|
+
* timeout the raw booking body is returned with an instruction to keep
|
|
1628
|
+
* polling get_inference_booking.
|
|
1629
|
+
*/
|
|
1630
|
+
async function pollInferenceBookingToOutcome(handle, timeoutMs) {
|
|
1631
|
+
const deadline = Date.now() + timeoutMs;
|
|
1632
|
+
for (;;) {
|
|
1633
|
+
if (Date.now() > deadline) {
|
|
1634
|
+
return {
|
|
1635
|
+
booking: { handle, status: "booking" },
|
|
1636
|
+
instruction: `The GPU booking is still concluding. Nothing is charged and no deployment exists until a GPU is confirmed. Call get_inference_booking with handle "${handle}" to continue polling.`,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
1640
|
+
let outcome;
|
|
1641
|
+
try {
|
|
1642
|
+
outcome = await client.api(`/api/inference/bookings/${encodeURIComponent(handle)}`);
|
|
1643
|
+
}
|
|
1644
|
+
catch (error) {
|
|
1645
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1646
|
+
// ADMISSION_UNAVAILABLE / 503: transient market outage — keep polling,
|
|
1647
|
+
// never fail-closed. Every other error (including the structured
|
|
1648
|
+
// CAPACITY_UNAVAILABLE miss) is terminal.
|
|
1649
|
+
if (text.includes("ADMISSION_UNAVAILABLE") || text.includes("error 503"))
|
|
1650
|
+
continue;
|
|
1651
|
+
throw error;
|
|
1652
|
+
}
|
|
1653
|
+
if (outcome?.booking?.status === "booking")
|
|
1654
|
+
continue;
|
|
1655
|
+
return outcome;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
server.tool("create_inference", "Create an OpenAI-compatible serving deployment using the exact canonical_request approved through preflight_inference, including hf_model_revision/base_model_revision. Serving mode, OpenAI task and image support are derived by the platform (not inputs) and come back on the response. BOOK-BEFORE-REVEAL: a non-queued create answers 202 with a booking handle while the GPU is secured on real capacity (30-40s typical); this tool polls it and returns the full payload (one-time inference_key) only after the GPU is confirmed — a definitive miss returns the structured CAPACITY_UNAVAILABLE with fresh available_gpus + minimum_requirement and NO deployment exists. Queueing occurs only with explicit allow_capacity_queue=true, max_price_hour_cents prevents a later price increase. Reuse idempotency_key after a timeout to recover the same booking/deployment.", deploymentCreateMutationToolSchema, async ({ idempotency_key, ...params }) => {
|
|
1659
|
+
if (params.source_type === "hf_model" && params.hf_model_id) {
|
|
1660
|
+
await assertModelHosted(params.hf_model_id, "create_inference");
|
|
1661
|
+
}
|
|
1662
|
+
const body = buildDeploymentBody(params);
|
|
1663
|
+
await assertInferenceGpuSelectionBookable(body);
|
|
1664
|
+
try {
|
|
1665
|
+
const data = await client.api("/api/inference", {
|
|
1666
|
+
method: "POST",
|
|
1667
|
+
body,
|
|
1668
|
+
headers: { "Idempotency-Key": idempotency_key },
|
|
1669
|
+
});
|
|
1670
|
+
if (data?.booking?.handle) {
|
|
1671
|
+
return json(await pollInferenceBookingToOutcome(String(data.booking.handle), 120_000));
|
|
1672
|
+
}
|
|
1673
|
+
return json(data);
|
|
1674
|
+
}
|
|
1675
|
+
catch (error) {
|
|
1676
|
+
throw modelResolveFailure(error, params.hf_model_id ?? params.base_model_id, "create_inference");
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1680
|
+
/* TOOL: get_inference_booking */
|
|
1681
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1682
|
+
server.tool("get_inference_booking", "Poll a book-before-reveal booking handle from create_inference. Returns {booking:{status:'booking'}} while the GPU is being confirmed, the full create payload (with the ONE-TIME inference_key, exactly once) once the GPU is secured, the structured CAPACITY_UNAVAILABLE error on a definitive miss (no deployment exists, nothing charged), or a retryable ADMISSION_UNAVAILABLE 503 during a transient market outage (never treat it as out-of-stock).", { handle: z.string().describe("Booking handle from create_inference's 202 response.") }, async ({ handle }) => {
|
|
1683
|
+
const data = await client.api(`/api/inference/bookings/${encodeURIComponent(handle)}`);
|
|
1684
|
+
return json(data);
|
|
1685
|
+
});
|
|
1686
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1687
|
+
/* TOOL: list_inferences */
|
|
1688
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1689
|
+
server.tool("list_inferences", "List one bounded, newest-first page of serving deployments in the API key's workspace. Reuse next_cursor with the same status/search filters to continue; start without a cursor after filters change. The returned lifecycle values are durable control-plane state, not a promise that an endpoint is live unless status is running.", {
|
|
1690
|
+
limit: z.number().int().min(1).max(200).optional().describe("Page size (default 50, maximum 200)."),
|
|
1691
|
+
cursor: z.string().max(1024).optional().describe("Opaque next_cursor from the preceding page."),
|
|
1692
|
+
status: z.enum([
|
|
1693
|
+
"all", "running", "provisioning", "stopped", "failed", "queued_capacity", "degraded",
|
|
1694
|
+
"paused_insufficient_funds", "crash_loop", "downloading_weights", "loading_model",
|
|
1695
|
+
]).optional(),
|
|
1696
|
+
search: z.string().max(120).optional().describe("Case-insensitive deployment-name prefix."),
|
|
1697
|
+
}, async ({ limit, cursor, status, search }) => {
|
|
1698
|
+
const params = new URLSearchParams();
|
|
1699
|
+
if (limit !== undefined)
|
|
1700
|
+
params.set("limit", String(limit));
|
|
1701
|
+
if (cursor)
|
|
1702
|
+
params.set("cursor", cursor);
|
|
1703
|
+
if (status && status !== "all")
|
|
1704
|
+
params.set("status", status);
|
|
1705
|
+
if (search?.trim())
|
|
1706
|
+
params.set("search", search.trim());
|
|
1707
|
+
const query = params.toString();
|
|
1708
|
+
const data = await client.api(`/api/inference${query ? `?${query}` : ""}`);
|
|
1709
|
+
return json(data);
|
|
1710
|
+
});
|
|
1711
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1712
|
+
/* TOOL: get_inference_status */
|
|
1713
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1714
|
+
server.tool("get_inference_status", "Read durable deployment status, desired state, loading progress, endpoint, errors, selected GPU/price cap, queue entry/expiry, notification summary, and wallet-authorization state. Also returns status_summary: the lifecycle phase, the GPU (count x SKU), whether the endpoint may be used, and the queue/GPU state, derived from this same response so a field that is null before the pod reports is not mistaken for 'no GPU' or 'no phase'. A queue deadline keeps status=failed for filter compatibility and sets status_reason/error_code=queue_expired. Treat queued_capacity as waiting and report the endpoint live only when status_summary.endpoint_usable is true. The derived serving settings (serving_mode, model_task, supports_images, context_length) are reported here too — they are platform-chosen, never caller-chosen.", { deployment_id: z.string().describe("Deployment ID.") }, async ({ deployment_id }) => {
|
|
1715
|
+
validateId(deployment_id, "deployment_id");
|
|
1716
|
+
const data = await client.api(`/api/inference/${deployment_id}`);
|
|
1717
|
+
// Same raw-text class as the training job detail (top-level last_error and
|
|
1718
|
+
// the nested queue.last_error). The control plane curates these today; this
|
|
1719
|
+
// is the boundary backstop, and it touches no other field.
|
|
1720
|
+
//
|
|
1721
|
+
// Redaction runs OUTSIDE the summariser deliberately: summarizeInferenceStatus
|
|
1722
|
+
// derives status_summary from these same raw fields, so redacting first would
|
|
1723
|
+
// let it summarise text the backstop had already cleaned — and redacting last
|
|
1724
|
+
// covers anything it copied forward.
|
|
1725
|
+
return json(redactFreeTextFields(summarizeInferenceStatus(data), RAW_FAILURE_TEXT_FIELDS));
|
|
1726
|
+
});
|
|
1727
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1728
|
+
/* TOOL: get_inference_metrics */
|
|
1729
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1730
|
+
server.tool("get_inference_metrics", "Read serving metrics for a deployment: request counts, prompt/generation token totals, cache hit rate, time to first token, inter-token latency, throughput, GPU utilization and memory, KV cache usage, and a recent time series. Counters are lifetime totals; the time series reflects the selected window. Responses are served from a short cache; zeros are real measurements, not missing data.", {
|
|
1731
|
+
deployment_id: z.string().describe("Deployment ID."),
|
|
1732
|
+
window: z
|
|
1733
|
+
.enum(["1h", "24h", "7d", "30d"])
|
|
1734
|
+
.optional()
|
|
1735
|
+
.describe("Time-series window (default 24h). Sets the series range and bucket size: 1h/1min, 24h/5min, 7d/30min, 30d/1h."),
|
|
1736
|
+
}, async ({ deployment_id, window }) => {
|
|
1737
|
+
validateId(deployment_id, "deployment_id");
|
|
1738
|
+
const params = new URLSearchParams();
|
|
1739
|
+
if (window)
|
|
1740
|
+
params.set("window", window);
|
|
1741
|
+
const query = params.toString();
|
|
1742
|
+
const data = await client.api(`/api/inference/${deployment_id}/metrics${query ? `?${query}` : ""}`);
|
|
1743
|
+
return json(data);
|
|
1744
|
+
});
|
|
1745
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1746
|
+
/* TOOL: get_inference_notifications */
|
|
1747
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1748
|
+
server.tool("get_inference_notifications", "Read the durable lifecycle-email history for a deployment. States are pending, sending, delivered, or dead_letter. Dead letters retain the bounded retry count and last error; deployment status remains authoritative even when email delivery fails.", {
|
|
1749
|
+
deployment_id: z.string().describe("Deployment ID."),
|
|
1750
|
+
limit: z.number().int().min(1).max(100).optional().describe("Newest rows to return (default 50, maximum 100)."),
|
|
1751
|
+
}, async ({ deployment_id, limit }) => {
|
|
1752
|
+
validateId(deployment_id, "deployment_id");
|
|
1753
|
+
const params = new URLSearchParams();
|
|
1754
|
+
if (limit !== undefined)
|
|
1755
|
+
params.set("limit", String(limit));
|
|
1756
|
+
const query = params.toString();
|
|
1757
|
+
const data = await client.api(`/api/inference/${deployment_id}/notifications${query ? `?${query}` : ""}`);
|
|
1758
|
+
return json(data);
|
|
1759
|
+
});
|
|
1760
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1761
|
+
/* TOOL: update_inference_policy */
|
|
1762
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1763
|
+
server.tool("update_inference_policy", "Update explicit capacity-queue consent or the maximum accepted total hourly GPU price. A cap below the current verified price is rejected. This call does not silently choose another GPU.", {
|
|
1764
|
+
deployment_id: z.string().describe("Deployment ID."),
|
|
1765
|
+
allow_capacity_queue: z.boolean().optional(),
|
|
1766
|
+
max_price_hour_cents: priceCapSchema(false),
|
|
1767
|
+
}, async ({ deployment_id, allow_capacity_queue, max_price_hour_cents }) => {
|
|
1768
|
+
validateId(deployment_id, "deployment_id");
|
|
1769
|
+
const data = await client.api(`/api/inference/${deployment_id}`, {
|
|
1770
|
+
method: "PATCH",
|
|
1771
|
+
body: buildDeploymentPolicyBody({ allow_capacity_queue, max_price_hour_cents }),
|
|
1772
|
+
});
|
|
1773
|
+
return json(data);
|
|
1774
|
+
});
|
|
1775
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1776
|
+
/* TOOL: stop_inference */
|
|
1777
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1778
|
+
server.tool("stop_inference", "Stop a deployment and release GPU capacity plus its wallet authorization. The acknowledgement means the durable stop workflow was accepted; poll get_inference_status until stopped or failed.", { deployment_id: z.string().describe("Deployment ID.") }, async ({ deployment_id }) => {
|
|
1779
|
+
validateId(deployment_id, "deployment_id");
|
|
1780
|
+
const data = await client.api(`/api/inference/${deployment_id}/stop`, { method: "POST" });
|
|
1781
|
+
return json(data);
|
|
1782
|
+
});
|
|
1783
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1784
|
+
/* TOOL: resume_inference */
|
|
1785
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1786
|
+
server.tool("resume_inference", "Atomically resume a stopped deployment. Exactly one concurrent caller can claim the lifecycle generation. The control plane revalidates the saved GPU price cap and wallet authorization before provisioning; capacity may queue only when the saved queue consent allows it.", { deployment_id: z.string().describe("Deployment ID.") }, async ({ deployment_id }) => {
|
|
1787
|
+
validateId(deployment_id, "deployment_id");
|
|
1788
|
+
const data = await client.api(`/api/inference/${deployment_id}/resume`, { method: "POST" });
|
|
1789
|
+
return json(data);
|
|
1790
|
+
});
|
|
1791
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1792
|
+
/* TOOL: restart_inference */
|
|
1793
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1794
|
+
server.tool("restart_inference", "Claim one fenced restart generation. A same-config running deployment restarts in place and never silently releases its GPU for a replacement when the platform cannot confirm that restart. Pending configuration changes and failed/crash-loop recovery explicitly provision a replacement. Poll status because the acknowledgement is not endpoint readiness.", { deployment_id: z.string().describe("Deployment ID.") }, async ({ deployment_id }) => {
|
|
1795
|
+
validateId(deployment_id, "deployment_id");
|
|
1796
|
+
const data = await client.api(`/api/inference/${deployment_id}/restart`, { method: "POST" });
|
|
1797
|
+
return json(data);
|
|
1798
|
+
});
|
|
1799
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1800
|
+
/* TOOL: delete_inference */
|
|
1801
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1802
|
+
server.tool("delete_inference", "Permanently delete a serving deployment. The control plane first verifies infrastructure teardown and records retryable orphan cleanup if needed, then releases/refunds the generation-scoped wallet authorization. This cannot be undone.", { deployment_id: z.string().describe("Deployment ID.") }, async ({ deployment_id }) => {
|
|
1803
|
+
validateId(deployment_id, "deployment_id");
|
|
1804
|
+
const data = await client.api(`/api/inference/${deployment_id}`, { method: "DELETE" });
|
|
1805
|
+
return json(data);
|
|
1806
|
+
});
|
|
1807
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1808
|
+
/* TOOL: list_datasets */
|
|
1809
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1810
|
+
server.tool("list_datasets", "List all datasets in your workspace. Returns dataset ID, name, format (JSONL/Parquet/CSV), row count, file size, column names, and creation date for each dataset.", {}, async () => {
|
|
1811
|
+
const data = await client.api("/api/datasets");
|
|
1812
|
+
return json(data);
|
|
1813
|
+
});
|
|
1814
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1815
|
+
/* TOOL: upload_dataset */
|
|
1816
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1817
|
+
// PRE-LAUNCH GATE (launchGates.datasets — src/config.ts): while datasets are
|
|
1818
|
+
// coming soon this tool is NOT REGISTERED — hidden from tools/list so an agent
|
|
1819
|
+
// never learns the capability exists. The service enforces the same gate
|
|
1820
|
+
// server-side (403 DATASETS_COMING_SOON) as the authority. At launch the flag
|
|
1821
|
+
// opens and the registration returns verbatim.
|
|
1822
|
+
if (!launchGates.datasets) {
|
|
1823
|
+
server.tool("upload_dataset", "Upload a dataset file for fine-tuning. Supports JSONL, Parquet, and CSV formats up to 500MB. The file is automatically validated: format detection, row counting, column identification, and schema validation. Returns the dataset ID needed for create_training_job.", {
|
|
1824
|
+
file_path: z.string().describe("Absolute path to the dataset file on disk"),
|
|
1825
|
+
name: z.string().optional().describe("Display name for the dataset (defaults to filename)"),
|
|
1826
|
+
}, async ({ file_path, name }) => {
|
|
1827
|
+
const absPath = resolve(file_path);
|
|
1828
|
+
const allowedExtensions = [".jsonl", ".parquet", ".csv", ".json", ".tsv", ".txt"];
|
|
1829
|
+
const ext = absPath.toLowerCase().slice(absPath.lastIndexOf("."));
|
|
1830
|
+
if (!allowedExtensions.includes(ext)) {
|
|
1831
|
+
throw new Error(`Unsupported file type: ${ext}. Allowed: ${allowedExtensions.join(", ")}`);
|
|
1832
|
+
}
|
|
1833
|
+
const normalizedPath = absPath.replace(/\\/g, "/");
|
|
1834
|
+
const sensitivePatterns = ["/etc/", "/.ssh/", "/.env", "/.git/", "/passwd", "/shadow", "/.aws/"];
|
|
1835
|
+
for (const pat of sensitivePatterns) {
|
|
1836
|
+
if (normalizedPath.includes(pat)) {
|
|
1837
|
+
throw new Error(`Access denied: cannot read files from restricted paths`);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
let fileStat;
|
|
1841
|
+
try {
|
|
1842
|
+
fileStat = await stat(absPath);
|
|
1843
|
+
}
|
|
1844
|
+
catch {
|
|
1845
|
+
throw new Error(`File not found: ${absPath}`);
|
|
1846
|
+
}
|
|
1847
|
+
if (fileStat.size > 500 * 1024 * 1024) {
|
|
1848
|
+
throw new Error(`File too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Maximum is 500MB.`);
|
|
1849
|
+
}
|
|
1850
|
+
const fileBuffer = await readFile(absPath);
|
|
1851
|
+
const fileName = name ?? basename(absPath);
|
|
1852
|
+
const blob = new Blob([fileBuffer]);
|
|
1853
|
+
const formData = new FormData();
|
|
1854
|
+
formData.append("file", blob, basename(absPath));
|
|
1855
|
+
formData.append("name", fileName);
|
|
1856
|
+
formData.append("size", fileStat.size.toString());
|
|
1857
|
+
const data = await client.api("/api/datasets/upload", {
|
|
1858
|
+
method: "POST",
|
|
1859
|
+
formData,
|
|
1860
|
+
});
|
|
1861
|
+
return json(data);
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1865
|
+
/* TOOL: preview_dataset */
|
|
1866
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1867
|
+
server.tool("preview_dataset", "Preview the first rows of a dataset to inspect its structure, column names, and content before training. Useful for verifying the dataset format is correct.", {
|
|
1868
|
+
dataset_id: z.string().describe("Dataset ID (e.g., 'ds_abc123')"),
|
|
1869
|
+
rows: z.number().optional().describe("Number of rows to preview (default: 10, max: 50)"),
|
|
1870
|
+
}, async ({ dataset_id, rows }) => {
|
|
1871
|
+
validateId(dataset_id, "dataset_id");
|
|
1872
|
+
const data = await client.api(`/api/datasets/${dataset_id}/preview`, {
|
|
1873
|
+
params: { page: "1", page_size: (rows ?? 10).toString() },
|
|
1874
|
+
});
|
|
1875
|
+
return json(data);
|
|
1876
|
+
});
|
|
1877
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1878
|
+
/* TOOL: delete_dataset */
|
|
1879
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1880
|
+
server.tool("delete_dataset", "Permanently delete a dataset from your workspace. This cannot be undone. Active training jobs using this dataset are not affected.", {
|
|
1881
|
+
dataset_id: z.string().describe("Dataset ID to delete"),
|
|
1882
|
+
}, async ({ dataset_id }) => {
|
|
1883
|
+
validateId(dataset_id, "dataset_id");
|
|
1884
|
+
await client.api(`/api/datasets/${dataset_id}`, { method: "DELETE" });
|
|
1885
|
+
return result(`Dataset ${dataset_id} deleted successfully.`);
|
|
1886
|
+
});
|
|
1887
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1888
|
+
/* TOOL: list_integrations */
|
|
1889
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1890
|
+
server.tool("list_integrations", "List the integrations connected to your workspace, such as Hugging Face accounts. Returns each integration's ID, label, provider type, and status. Use an integration ID wherever a tool accepts integration_id: importing HuggingFace datasets. This is how you pick WHICH connected account an import should use when several are connected. This read is workspace-scoped: without workspace context it fails with WORKSPACE_CONTEXT_REQUIRED, which is a configuration fix (BIOS_WORKSPACE_ID or a workspace-bound API key), not something to retry.", {}, async () => {
|
|
1891
|
+
const data = await client.api("/api/datasets/integrations");
|
|
1892
|
+
return json(data);
|
|
1893
|
+
});
|
|
1894
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1895
|
+
/* TOOL: import_huggingface_dataset */
|
|
1896
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1897
|
+
// PRE-LAUNCH GATE (launchGates.datasets — src/config.ts): not registered while
|
|
1898
|
+
// gated, exactly like upload_dataset above; the service's 403
|
|
1899
|
+
// DATASETS_COMING_SOON stays the authority.
|
|
1900
|
+
if (!launchGates.datasets) {
|
|
1901
|
+
server.tool("import_huggingface_dataset", "Import a dataset from HuggingFace Hub into your workspace. Works with public datasets and private datasets if your HuggingFace account is connected via Integrations. The dataset is downloaded, validated, and stored in your workspace. Returns the dataset ID for use with create_training_job.", {
|
|
1902
|
+
repo_id: z
|
|
1903
|
+
.string()
|
|
1904
|
+
.describe("HuggingFace dataset repo (e.g., 'databricks/dolly-15k', 'tatsu-lab/alpaca')"),
|
|
1905
|
+
integration_id: z
|
|
1906
|
+
.string()
|
|
1907
|
+
.describe("HuggingFace integration ID from your connected integrations"),
|
|
1908
|
+
split: z.string().optional().describe("Dataset split (default: 'train')"),
|
|
1909
|
+
max_samples: z.number().optional().describe("Max rows to import (imports all if omitted)"),
|
|
1910
|
+
name: z.string().optional().describe("Display name for the imported dataset"),
|
|
1911
|
+
}, async ({ repo_id, integration_id, split, max_samples, name }) => {
|
|
1912
|
+
const data = await client.api(`/api/datasets/integrations/${encodeURIComponent(integration_id)}/import`, {
|
|
1913
|
+
method: "POST",
|
|
1914
|
+
body: {
|
|
1915
|
+
dataset_id: repo_id,
|
|
1916
|
+
split: split ?? "train",
|
|
1917
|
+
max_samples,
|
|
1918
|
+
name: name ?? repo_id.split("/").pop(),
|
|
1919
|
+
},
|
|
1920
|
+
});
|
|
1921
|
+
return json(data);
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1925
|
+
/* TOOL: get_training_capabilities */
|
|
1926
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1927
|
+
server.tool("get_training_capabilities", "Return the authoritative training contract Run BiOS enforces today: enabled and disabled methods, algorithms and adapters; exact hyperparameter names, aliases, types, defaults, enums and ranges; dependencies and incompatibilities. contract_version/schema_version identify the contract you are reading. Call this before constructing an automated training payload instead of relying on a hard-coded client list.", {}, async () => {
|
|
1928
|
+
const data = await client.api("/api/training/capabilities");
|
|
1929
|
+
// The contract used to carry image_compatibility — the training-engine image
|
|
1930
|
+
// VERSION range it was written against. training-service no longer emits it;
|
|
1931
|
+
// this strip stays as a backstop because a customer cannot select a build and
|
|
1932
|
+
// must never learn one, and contract_version/schema_version already version
|
|
1933
|
+
// the contract for clients.
|
|
1934
|
+
return json(stripBuildIdentity(data));
|
|
1935
|
+
});
|
|
1936
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1937
|
+
/* TOOL: preflight_training_job */
|
|
1938
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1939
|
+
server.tool("preflight_training_job", "Validate and canonicalize a proposed training job without creating a job, charging the wallet, entering the queue, or allocating a GPU. Returns dataset compatibility, model facts, authoritative GPU choices/prices, queue eligibility, alternatives, warnings, and a request hash.", {
|
|
1940
|
+
model: z.string().describe("Base model id from the Run BiOS catalog (e.g., 'meta-llama/Llama-3.1-8B-Instruct'). Must be hosted on Run BiOS — pick from list_models/search_models. Training always starts from a catalog base model; training FROM a fine-tuned model (adapter stacking) is not supported."),
|
|
1941
|
+
model_revision: z.string().max(256).optional().describe("Requested model branch, tag, or commit. Copy the exact 40-hex value returned in canonical_request into create."),
|
|
1942
|
+
dataset_id: z.string().optional(),
|
|
1943
|
+
dataset_ids: z.array(z.string()).min(1).optional(),
|
|
1944
|
+
method: z.enum(TRAINING_METHODS),
|
|
1945
|
+
adapter: z.enum(TRAINING_ADAPTERS).optional(),
|
|
1946
|
+
gpu_type: z.string().optional(),
|
|
1947
|
+
gpu_count: z.number().int().min(1).max(8).optional(),
|
|
1948
|
+
gpu_priorities: z
|
|
1949
|
+
.array(z.object({
|
|
1950
|
+
gpu_type: z.string(), gpu_count: z.number().int().min(1).max(8),
|
|
1951
|
+
provider: z.string().min(1).max(30).optional(), region: z.string().min(1).max(64).optional(),
|
|
1952
|
+
tier: z.literal("secure").optional(),
|
|
1953
|
+
}))
|
|
1954
|
+
.min(1)
|
|
1955
|
+
.max(5)
|
|
1956
|
+
.optional()
|
|
1957
|
+
.describe("Ranked GPU placement choices. Copy these unchanged from the preflight response's accepted choices; the platform fills in and manages infrastructure placement automatically. Queueing requires 3-5 distinct entries; entry one must match gpu_type/gpu_count."),
|
|
1958
|
+
queue_if_unavailable: z.boolean().optional().describe("Explicit consent to wait in the capacity queue when none of the ranked GPU choices are free; requires 3 to 5 gpu_priorities."),
|
|
1959
|
+
queue_deadline: z.string().min(1).optional().describe("Optional RFC 3339 deadline from one minute through seven days in the future; requires queue_if_unavailable=true."),
|
|
1960
|
+
max_price_hour_cents: priceCapSchema(true),
|
|
1961
|
+
storage_gb: z.number().int().min(1).max(10000).optional(),
|
|
1962
|
+
num_checkpoints: z.number().int().min(1).max(100).optional(),
|
|
1963
|
+
integration_id: z.string().optional(),
|
|
1964
|
+
network_volume_id: z.string().optional(),
|
|
1965
|
+
cache_dataset: z.boolean().optional(),
|
|
1966
|
+
dataset_mixing: z.enum(["shuffle", "sequential", "interleave"]).optional(),
|
|
1967
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
1968
|
+
}, async (params) => {
|
|
1969
|
+
await assertModelHosted(params.model, "preflight_training_job");
|
|
1970
|
+
const body = buildTrainingCreateBody(params);
|
|
1971
|
+
const data = await client.api("/api/training/preflight", { method: "POST", body });
|
|
1972
|
+
return json(data);
|
|
1973
|
+
});
|
|
1974
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1975
|
+
/* TOOL: create_training_job */
|
|
1976
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
1977
|
+
// PRE-LAUNCH GATE (launchGates.training — src/config.ts): while fine-tuning is
|
|
1978
|
+
// coming soon this tool is NOT REGISTERED — hidden from tools/list so an agent
|
|
1979
|
+
// never learns the capability exists. The service enforces the same gate
|
|
1980
|
+
// server-side (403 TRAINING_COMING_SOON before any billing or booking) as the
|
|
1981
|
+
// authority. At launch the flag opens and the registration returns verbatim.
|
|
1982
|
+
if (!launchGates.training) {
|
|
1983
|
+
server.tool("create_training_job", "Create a fine-tuning job using the same contract as the web UI, following BOOK-BEFORE-REVEAL: the call blocks while the ranked GPU ladder is booked (~40s). A job id and the started email exist only once a real pod is secured, so status is 'booked' (booked==secured; training then provisions/downloads/runs on its own) or 'securing' (still booking at the deadline — poll get_training_status until booked/running). With explicit queue consent it may return 'queued'. A definitive booking-time miss returns the structured CAPACITY_UNAVAILABLE (409) with fresh available_gpus and NO job exists (nothing charged); never auto-substitute a GPU. Call preflight_training_job first, preserve the accepted ranked GPU choices, queue deadline, and maximum total hourly price, then reuse one idempotency key after timeouts.", {
|
|
1984
|
+
model: z.string().describe("Base model id from the Run BiOS catalog (e.g., 'meta-llama/Llama-3.1-8B-Instruct'). Must be hosted on Run BiOS — pick from list_models/search_models. Training always starts from a catalog base model; training FROM a fine-tuned model (adapter stacking) is not supported."),
|
|
1985
|
+
model_revision: z.string().max(256).optional().describe("Exact model_revision from preflight canonical_request; a branch or tag is accepted but resolved again before any paid mutation."),
|
|
1986
|
+
idempotency_key: z
|
|
1987
|
+
.string()
|
|
1988
|
+
.min(8)
|
|
1989
|
+
.max(128)
|
|
1990
|
+
.regex(/^[A-Za-z0-9._:-]+$/)
|
|
1991
|
+
.optional()
|
|
1992
|
+
.describe("Stable retry key. Reuse it after a timeout; omit to generate one for this tool call."),
|
|
1993
|
+
dataset_id: z.string().optional().describe("Compatibility field for one dataset ID."),
|
|
1994
|
+
dataset_ids: z.array(z.string()).min(1).optional().describe("One or more dataset IDs to compose for training."),
|
|
1995
|
+
method: z.enum(TRAINING_METHODS).describe("SFT/PT or an offline preference/reward method."),
|
|
1996
|
+
adapter: z.enum(TRAINING_ADAPTERS).optional().describe("Any adapter supported by the Run BiOS contract."),
|
|
1997
|
+
gpu_type: z.string().optional().describe("GPU type (e.g., 'A100_80GB'). Use get_recommended_gpu to find the best option. Auto-selected if omitted."),
|
|
1998
|
+
gpu_count: z.number().int().min(1).max(8).optional().describe("Number of GPUs for the primary choice."),
|
|
1999
|
+
gpu_priorities: z
|
|
2000
|
+
.array(z.object({
|
|
2001
|
+
gpu_type: z.string(), gpu_count: z.number().int().min(1).max(8),
|
|
2002
|
+
provider: z.string().min(1).max(30).optional(), region: z.string().min(1).max(64).optional(),
|
|
2003
|
+
tier: z.literal("secure").optional(),
|
|
2004
|
+
}))
|
|
2005
|
+
.min(1)
|
|
2006
|
+
.max(5)
|
|
2007
|
+
.optional()
|
|
2008
|
+
.describe("Ranked GPU placement choices. Copy these unchanged from the preflight response's accepted choices; the platform fills in and manages infrastructure placement automatically. Queueing requires 3-5 distinct entries; entry one must match gpu_type/gpu_count."),
|
|
2009
|
+
queue_if_unavailable: z.boolean().optional().describe("Explicit consent to wait in the capacity queue when none of the ranked GPU choices are free; requires 3 to 5 gpu_priorities."),
|
|
2010
|
+
queue_deadline: z.string().min(1).optional().describe("Optional RFC 3339 deadline from one minute through seven days in the future; requires queue_if_unavailable=true."),
|
|
2011
|
+
max_price_hour_cents: priceCapSchema(true),
|
|
2012
|
+
storage_gb: z.number().int().min(1).max(10000).optional(),
|
|
2013
|
+
num_checkpoints: z.number().int().min(1).max(100).optional(),
|
|
2014
|
+
epochs: z.number().positive().max(1000).optional().describe("Number of training epochs."),
|
|
2015
|
+
learning_rate: z.number().positive().max(1).optional().describe("Learning rate."),
|
|
2016
|
+
batch_size: z.number().int().min(1).max(65536).optional().describe("Per-device train batch size."),
|
|
2017
|
+
gradient_accumulation_steps: z.number().int().min(1).max(65536).optional(),
|
|
2018
|
+
max_seq_length: trainingSeqLengthSchema(),
|
|
2019
|
+
lora_rank: z.number().int().min(1).max(4096).optional(),
|
|
2020
|
+
lora_alpha: z.number().positive().max(1_000_000).optional(),
|
|
2021
|
+
warmup_ratio: z.number().min(0).max(1).optional(),
|
|
2022
|
+
weight_decay: z.number().min(0).max(10).optional(),
|
|
2023
|
+
scheduler: z
|
|
2024
|
+
.enum(["linear", "cosine", "cosine_with_restarts", "polynomial", "constant", "constant_with_warmup"])
|
|
2025
|
+
.optional()
|
|
2026
|
+
.describe("Learning-rate scheduler."),
|
|
2027
|
+
job_name: z.string().max(255).optional(),
|
|
2028
|
+
eval_split_ratio: z.number().min(0).max(0.99).optional(),
|
|
2029
|
+
integration_id: z.string().optional(),
|
|
2030
|
+
network_volume_id: z.string().optional(),
|
|
2031
|
+
cache_dataset: z.boolean().optional(),
|
|
2032
|
+
dataset_mixing: z.enum(["shuffle", "sequential", "interleave"]).optional(),
|
|
2033
|
+
config: z.record(z.string(), z.unknown()).optional().describe("Additional Run BiOS training configuration fields."),
|
|
2034
|
+
}, async (params) => {
|
|
2035
|
+
await assertModelHosted(params.model, "create_training_job");
|
|
2036
|
+
const body = buildTrainingCreateBody(params);
|
|
2037
|
+
const data = await client.api("/api/training/jobs", {
|
|
2038
|
+
method: "POST",
|
|
2039
|
+
body,
|
|
2040
|
+
headers: { "Idempotency-Key": params.idempotency_key ?? randomUUID() },
|
|
2041
|
+
});
|
|
2042
|
+
return json(data);
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2046
|
+
/* TOOL: list_training_jobs */
|
|
2047
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2048
|
+
server.tool("list_training_jobs", "List all training jobs with their status, progress percentage, base model, training method, GPU type, cost so far, and timestamps. Filter by status to find running, completed, or stopped jobs.", {
|
|
2049
|
+
status: z
|
|
2050
|
+
.enum(["running", "completed", "stopped", "queued", "securing", "booked", "interrupted", "failed"])
|
|
2051
|
+
.optional()
|
|
2052
|
+
.describe("Filter by job status"),
|
|
2053
|
+
}, async ({ status }) => {
|
|
2054
|
+
const data = await client.api("/api/training/jobs", { params: { status } });
|
|
2055
|
+
return json(data);
|
|
2056
|
+
});
|
|
2057
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2058
|
+
/* TOOL: get_training_status */
|
|
2059
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2060
|
+
server.tool("get_training_status", "Get detailed status for a specific training job: progress percentage, current step/total steps, current loss, learning rate, elapsed time, estimated time remaining, GPU utilization, and job configuration. Status 'securing' means a create is still booking a pod (poll until 'booked'); 'booked' means the GPU is secured and the job is starting. 'interrupted' is a self-heal rest state after a started job loses its pod — billing is already stopped and it is distinct from 'failed'; call resume_training_job to continue from the last checkpoint.", {
|
|
2061
|
+
job_id: z.string().describe("Training job ID (e.g., 'job_abc123')"),
|
|
2062
|
+
}, async ({ job_id }) => {
|
|
2063
|
+
validateId(job_id, "job_id");
|
|
2064
|
+
const data = await client.api(`/api/training/jobs/${job_id}`);
|
|
2065
|
+
// A stopped job's stop_last_error is the raw transport error from the last
|
|
2066
|
+
// call to the machine, so it carries the request URL and with it the
|
|
2067
|
+
// supplier's hostname. Redact the raw-text fields only; model ids, config
|
|
2068
|
+
// and every other value pass through untouched.
|
|
2069
|
+
return json(redactFreeTextFields(data, RAW_FAILURE_TEXT_FIELDS));
|
|
2070
|
+
});
|
|
2071
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2072
|
+
/* TOOL: get_training_metrics */
|
|
2073
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2074
|
+
server.tool("get_training_metrics", "Get training metrics history for a job: training loss over time, eval loss, learning rate schedule, and evaluation results. Use this to check if training is progressing well (loss should decrease) or if the model is overfitting (eval loss increasing while train loss decreases).", {
|
|
2075
|
+
job_id: z.string().describe("Training job ID"),
|
|
2076
|
+
}, async ({ job_id }) => {
|
|
2077
|
+
validateId(job_id, "job_id");
|
|
2078
|
+
const data = await client.api(`/api/training/jobs/${job_id}/metrics`);
|
|
2079
|
+
return json(data);
|
|
2080
|
+
});
|
|
2081
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2082
|
+
/* TOOL: get_training_logs */
|
|
2083
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2084
|
+
server.tool("get_training_logs", "Get structured recent training log entries (level, message, timestamp). Results are newest first and bounded by tail.", {
|
|
2085
|
+
job_id: z.string().describe("Training job ID"),
|
|
2086
|
+
tail: z
|
|
2087
|
+
.number()
|
|
2088
|
+
.int()
|
|
2089
|
+
.min(1)
|
|
2090
|
+
.max(1000)
|
|
2091
|
+
.optional()
|
|
2092
|
+
.describe("Number of most recent log entries to return (default: 200, max: 1000)"),
|
|
2093
|
+
}, async ({ job_id, tail }) => {
|
|
2094
|
+
validateId(job_id, "job_id");
|
|
2095
|
+
const data = await client.api(`/api/training/jobs/${job_id}/logs`, {
|
|
2096
|
+
params: { tail: tail?.toString() },
|
|
2097
|
+
});
|
|
2098
|
+
// Log lines are written by the training engine, so a boot/pull line can
|
|
2099
|
+
// quote the image reference it started from. Redact by content: the
|
|
2100
|
+
// customer keeps every line, minus any build identity inside it.
|
|
2101
|
+
return json(redactBuildIdentityDeep(trimTrainingLogs(data, tail)));
|
|
2102
|
+
});
|
|
2103
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2104
|
+
/* TOOL: stop_training_job */
|
|
2105
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2106
|
+
server.tool("stop_training_job", "Request a durable stop for an active training job. The acknowledgement means the stop workflow was accepted; checkpoint preservation and resumability are reported by later job/checkpoint state, not promised by this call.", {
|
|
2107
|
+
job_id: z.string().describe("Training job ID to stop"),
|
|
2108
|
+
keep_data: z.boolean().optional().describe("Attempt to preserve checkpoint/output data (default: true)."),
|
|
2109
|
+
}, async ({ job_id, keep_data }) => {
|
|
2110
|
+
validateId(job_id, "job_id");
|
|
2111
|
+
const data = await client.api(`/api/training/jobs/${job_id}/stop`, {
|
|
2112
|
+
method: "POST",
|
|
2113
|
+
body: { keep_data: keep_data ?? true },
|
|
2114
|
+
});
|
|
2115
|
+
return json(data);
|
|
2116
|
+
});
|
|
2117
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2118
|
+
/* TOOL: resume_training_job */
|
|
2119
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2120
|
+
server.tool("resume_training_job", "Resume a stopped or 'interrupted' training job from its latest checkpoint when the server has verified it as resumable. 'interrupted' is the self-heal rest state a started job enters after losing its pod (billing already stopped, distinct from 'failed'). Resume re-books on secured capacity before reporting resumed (book-before-reveal still applies). The response identifies the job; exact optimizer/epoch continuation depends on the checkpoint manifest and is not assumed by this tool.", {
|
|
2121
|
+
job_id: z.string().describe("Training job ID to resume"),
|
|
2122
|
+
idempotency_key: z
|
|
2123
|
+
.string()
|
|
2124
|
+
.min(8)
|
|
2125
|
+
.max(128)
|
|
2126
|
+
.regex(/^[A-Za-z0-9._:-]+$/)
|
|
2127
|
+
.optional()
|
|
2128
|
+
.describe("Stable retry key. Reuse it after a timeout; omit to generate one for this tool call."),
|
|
2129
|
+
}, async ({ job_id, idempotency_key }) => {
|
|
2130
|
+
validateId(job_id, "job_id");
|
|
2131
|
+
const data = await client.api(`/api/training/jobs/${job_id}/resume`, {
|
|
2132
|
+
method: "POST",
|
|
2133
|
+
headers: { "Idempotency-Key": idempotency_key ?? randomUUID() },
|
|
2134
|
+
});
|
|
2135
|
+
return json(data);
|
|
2136
|
+
});
|
|
2137
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2138
|
+
/* TOOL: get_training_checkpoints */
|
|
2139
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2140
|
+
server.tool("get_training_checkpoints", "List recorded checkpoints for a training job. Returns checkpoint identity, step/epoch, size and final/best flags. Use get_checkpoint_download_manifest for file URLs; this list does not fabricate loss, eval metrics, or download URLs.", {
|
|
2141
|
+
job_id: z.string().describe("Training job ID"),
|
|
2142
|
+
}, async ({ job_id }) => {
|
|
2143
|
+
validateId(job_id, "job_id");
|
|
2144
|
+
const data = await client.api(`/api/training/jobs/${job_id}/checkpoints`);
|
|
2145
|
+
return json(data);
|
|
2146
|
+
});
|
|
2147
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2148
|
+
/* TOOL: get_checkpoint_download_manifest */
|
|
2149
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2150
|
+
server.tool("get_checkpoint_download_manifest", "Get the server-generated per-file download manifest for a checkpoint. URLs are short-lived. Large checkpoints should be downloaded as individual resumable files and verified against the returned sizes/checksums when present.", {
|
|
2151
|
+
job_id: z.string().describe("Training job ID"),
|
|
2152
|
+
checkpoint_id: z.string().describe("Checkpoint ID from get_training_checkpoints"),
|
|
2153
|
+
}, async ({ job_id, checkpoint_id }) => {
|
|
2154
|
+
validateId(job_id, "job_id");
|
|
2155
|
+
validateId(checkpoint_id, "checkpoint_id");
|
|
2156
|
+
const data = await client.api(`/api/training/jobs/${job_id}/checkpoints/${checkpoint_id}/download`);
|
|
2157
|
+
return json(data);
|
|
2158
|
+
});
|
|
2159
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2160
|
+
/* TOOL: delete_training_checkpoint */
|
|
2161
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2162
|
+
server.tool("delete_training_checkpoint", "Delete a checkpoint after the API verifies ownership. This is destructive and can remove the ability to deploy or resume from that checkpoint.", {
|
|
2163
|
+
job_id: z.string().describe("Training job ID"),
|
|
2164
|
+
checkpoint_id: z.string().describe("Checkpoint ID"),
|
|
2165
|
+
}, async ({ job_id, checkpoint_id }) => {
|
|
2166
|
+
validateId(job_id, "job_id");
|
|
2167
|
+
validateId(checkpoint_id, "checkpoint_id");
|
|
2168
|
+
const data = await client.api(`/api/training/jobs/${job_id}/checkpoints/${checkpoint_id}`, {
|
|
2169
|
+
method: "DELETE",
|
|
2170
|
+
});
|
|
2171
|
+
return json(data);
|
|
2172
|
+
});
|
|
2173
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2174
|
+
/* TOOL: get_training_evals */
|
|
2175
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2176
|
+
server.tool("get_training_evals", "Get evaluation results for a training job. Returns benchmark scores, eval metrics, and model quality assessments computed during or after training.", {
|
|
2177
|
+
job_id: z.string().describe("Training job ID"),
|
|
2178
|
+
}, async ({ job_id }) => {
|
|
2179
|
+
validateId(job_id, "job_id");
|
|
2180
|
+
const data = await client.api(`/api/training/jobs/${job_id}/evals`);
|
|
2181
|
+
return json(data);
|
|
2182
|
+
});
|
|
2183
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2184
|
+
/* TOOL: chat_with_inference */
|
|
2185
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
2186
|
+
// The schema is capability-aware per deployment (honest advertisement only —
|
|
2187
|
+
// the runtime is gated server-side). When the inference key's deployment is
|
|
2188
|
+
// known to be text-only (no tool parser), the tools/tool_choice/
|
|
2189
|
+
// parallel_tool_calls params are omitted so a caller never offers tools a
|
|
2190
|
+
// text-only model can't call. When it is known to accept no image input, the
|
|
2191
|
+
// messages description says so. Unknown capabilities (undefined) advertise the
|
|
2192
|
+
// full schema (fail-open) — the /v1 endpoint still enforces.
|
|
2193
|
+
const inferenceSupportsTools = deploymentCaps?.supportsTools !== false;
|
|
2194
|
+
const inferenceMessagesDesc = deploymentCaps?.supportsImages === false
|
|
2195
|
+
? "OpenAI chat messages (text-only — this deployment does not accept image input), including assistant tool_calls and matching role=tool responses."
|
|
2196
|
+
: "OpenAI chat messages, including assistant tool_calls and matching role=tool responses.";
|
|
2197
|
+
const chatWithInferenceDescription = "Call a model through the unified OpenAI-compatible /v1 endpoint. Pass a serverless catalog model id (author/name) to call it by name using a workspace platform key (RUNBIOS_API_KEY with serverless scope), or set RUNBIOS_INFERENCE_KEY to target a dedicated deployment (with or without a model override). Supports complete tool definitions, assistant tool calls, and tool response messages. Set stream=true to consume SSE and receive one aggregated completion with reasoning surfaced as produced. Calls are never retried automatically.";
|
|
2198
|
+
// Shared, always-present params. The tools params are added only for a
|
|
2199
|
+
// deployment that can call tools — kept as two concrete literals (not a Record)
|
|
2200
|
+
// so each registration's `input` stays precisely typed for buildInferenceBody.
|
|
2201
|
+
const chatBaseSchema = {
|
|
2202
|
+
messages: z
|
|
2203
|
+
.array(z.record(z.string(), z.unknown()))
|
|
2204
|
+
.min(1)
|
|
2205
|
+
.describe(inferenceMessagesDesc),
|
|
2206
|
+
model: z
|
|
2207
|
+
.string()
|
|
2208
|
+
.optional()
|
|
2209
|
+
.describe("Model to call on the unified /v1 endpoint. For a serverless catalog model pass its id "
|
|
2210
|
+
+ "(author/name) and the gateway routes it; for a dedicated deployment pass its name/id, or "
|
|
2211
|
+
+ "omit when a dedicated inference key uniquely selects the deployment."),
|
|
2212
|
+
// Completion length had the same defect the context bounds had: `int()` alone
|
|
2213
|
+
// advertised MAX_SAFE_INTEGER, so the schema told an agent that asking for
|
|
2214
|
+
// nine quadrillion tokens is a legal request. The real ceiling is the SERVED
|
|
2215
|
+
// window minus the prompt, which no static schema can express, so the bound
|
|
2216
|
+
// is the same sanity ceiling serving uses and the description names the
|
|
2217
|
+
// authority.
|
|
2218
|
+
max_tokens: z
|
|
2219
|
+
.number()
|
|
2220
|
+
.int({ error: "max_tokens must be a whole number of tokens." })
|
|
2221
|
+
.min(1, { error: "max_tokens must be at least 1 — omit it to let the endpoint choose." })
|
|
2222
|
+
.max(CONTEXT_TOOL_MAX, {
|
|
2223
|
+
error: (issue) => `max_tokens ${String(issue.input)} is above the ${CONTEXT_TOOL_MAX}-token sanity bound of this tool. `
|
|
2224
|
+
+ MAX_TOKENS_POLICY_TEXT,
|
|
2225
|
+
})
|
|
2226
|
+
.optional()
|
|
2227
|
+
.describe(MAX_TOKENS_POLICY_TEXT),
|
|
2228
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
2229
|
+
top_p: z.number().min(0).max(1).optional(),
|
|
2230
|
+
response_format: z.record(z.string(), z.unknown()).optional(),
|
|
2231
|
+
reasoning_effort: z
|
|
2232
|
+
.enum(["none", "minimal", "low", "medium", "high", "max"])
|
|
2233
|
+
.optional()
|
|
2234
|
+
.describe("Standardized reasoning effort. Use 'none' to disable reasoning where the model allows it."),
|
|
2235
|
+
stream: z
|
|
2236
|
+
.boolean()
|
|
2237
|
+
.optional()
|
|
2238
|
+
.describe("Request server-sent streaming. The tool consumes the SSE and returns one aggregated "
|
|
2239
|
+
+ "completion (content + reasoning_content surfaced as produced, tool calls reassembled). "
|
|
2240
|
+
+ "Defaults to false."),
|
|
2241
|
+
idempotency_key: z
|
|
2242
|
+
.string()
|
|
2243
|
+
.min(1)
|
|
2244
|
+
.max(255)
|
|
2245
|
+
.optional()
|
|
2246
|
+
.describe("Stable caller-generated key to reuse only when explicitly retrying the same POST."),
|
|
2247
|
+
request_id: z.string().min(1).max(255).optional(),
|
|
2248
|
+
};
|
|
2249
|
+
if (inferenceSupportsTools) {
|
|
2250
|
+
server.tool("chat_with_inference", chatWithInferenceDescription, {
|
|
2251
|
+
...chatBaseSchema,
|
|
2252
|
+
tools: z
|
|
2253
|
+
.array(z.record(z.string(), z.unknown()))
|
|
2254
|
+
.optional()
|
|
2255
|
+
.describe("OpenAI function-tool definitions with JSON Schema object parameters."),
|
|
2256
|
+
tool_choice: z
|
|
2257
|
+
.union([
|
|
2258
|
+
z.enum(["none", "auto", "required"]),
|
|
2259
|
+
z.object({
|
|
2260
|
+
type: z.literal("function"),
|
|
2261
|
+
function: z.object({ name: z.string().min(1).max(64) }),
|
|
2262
|
+
}),
|
|
2263
|
+
])
|
|
2264
|
+
.optional(),
|
|
2265
|
+
parallel_tool_calls: z.boolean().optional(),
|
|
2266
|
+
}, async (input, extra) => {
|
|
2267
|
+
const body = buildInferenceBody(input);
|
|
2268
|
+
const data = await client.inferenceApi(body, input.idempotency_key, input.request_id, extra.signal);
|
|
2269
|
+
return json(data);
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
else {
|
|
2273
|
+
// Text-only deployment: no tools/tool_choice params advertised.
|
|
2274
|
+
server.tool("chat_with_inference", chatWithInferenceDescription, chatBaseSchema, async (input, extra) => {
|
|
2275
|
+
const body = buildInferenceBody(input);
|
|
2276
|
+
const data = await client.inferenceApi(body, input.idempotency_key, input.request_id, extra.signal);
|
|
2277
|
+
return json(data);
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
return mcp;
|
|
2281
|
+
}
|
|
2282
|
+
//# sourceMappingURL=server.js.map
|