@tuned-tensor/cli 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/chunk-DAP2Z7EZ.js +4400 -0
- package/dist/chunk-DAP2Z7EZ.js.map +1 -0
- package/dist/index.js +324 -70
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +1630 -0
- package/dist/local-runtime.js.map +1 -0
- package/docs/local-runtime/architecture.md +91 -0
- package/docs/local-runtime/spark.md +107 -0
- package/examples/local-runtime/dry-runner.json +14 -0
- package/examples/local-runtime/general-regression.jsonl +4 -0
- package/examples/local-runtime/local-runner.json +25 -0
- package/examples/local-runtime/smoke-spec.json +41 -0
- package/package.json +15 -5
- package/training/local-runner/pyproject.toml +25 -0
- package/training/local-runner/src/evaluate.py +274 -0
- package/training/local-runner/src/model_contract.py +47 -0
- package/training/local-runner/src/prefetch.py +192 -0
- package/training/local-runner/src/serve.py +272 -0
- package/training/local-runner/src/sft_data.py +97 -0
- package/training/local-runner/src/train.py +289 -0
- package/training/local-runner/uv.lock +1204 -0
|
@@ -0,0 +1,1630 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_LOCAL_SPEC_PATH,
|
|
4
|
+
assertArtifactManifest,
|
|
5
|
+
assertLocalRunInputReady,
|
|
6
|
+
assertUsableModelArtifact,
|
|
7
|
+
baseModelRevisionSchema,
|
|
8
|
+
behaviorSpecExampleSchema,
|
|
9
|
+
buildBundledPythonCommand,
|
|
10
|
+
buildModelPrefetchPayload,
|
|
11
|
+
buildSystemMessage,
|
|
12
|
+
canonicalLocalPipeline,
|
|
13
|
+
comparisonReportSchema,
|
|
14
|
+
compileSpecToJsonl,
|
|
15
|
+
createLocalStore,
|
|
16
|
+
datasetFormatSchema,
|
|
17
|
+
datasetPrebuiltSchema,
|
|
18
|
+
defaultLocalHome,
|
|
19
|
+
evalExampleResultSchema,
|
|
20
|
+
evalReportSchema,
|
|
21
|
+
evalSplitSchema,
|
|
22
|
+
evaluateGeneralRegressionGate,
|
|
23
|
+
evaluationSuiteFromChatJsonl,
|
|
24
|
+
exampleToChatRow,
|
|
25
|
+
examplesFromChatJsonl,
|
|
26
|
+
examplesFromSpec,
|
|
27
|
+
fineTuneHyperparametersSchema,
|
|
28
|
+
fineTuneRunRequestSchema,
|
|
29
|
+
fingerprintLocalBaseModel,
|
|
30
|
+
forwardStreamLines,
|
|
31
|
+
generatedPlaceholderIssues,
|
|
32
|
+
initLocalRunnerConfigFile,
|
|
33
|
+
initLocalSpecFile,
|
|
34
|
+
isTerminalRunState,
|
|
35
|
+
jsonFieldMetricsSchema,
|
|
36
|
+
loadJsonFile,
|
|
37
|
+
loadLocalRunInput,
|
|
38
|
+
loadLocalRunnerConfig,
|
|
39
|
+
loadRunRequest,
|
|
40
|
+
localBehaviorSpecFileSchema,
|
|
41
|
+
localRunnerConfigSchema,
|
|
42
|
+
localRuntimePackageRoot,
|
|
43
|
+
localStorePaths,
|
|
44
|
+
minimalMachineLearningEnvironment,
|
|
45
|
+
normalizeChatJsonlForRelocation,
|
|
46
|
+
prefetchBaseModel,
|
|
47
|
+
reportInBackground,
|
|
48
|
+
resolveLocalRunInputPaths,
|
|
49
|
+
resolveTrainingModel,
|
|
50
|
+
runLocalFineTune,
|
|
51
|
+
runLocalPipeline,
|
|
52
|
+
runReportSchema,
|
|
53
|
+
runRequestFromLocalSpec,
|
|
54
|
+
sanitizeLogLine,
|
|
55
|
+
specSnapshotSchema,
|
|
56
|
+
tokenF1,
|
|
57
|
+
trainingReportSchema,
|
|
58
|
+
validateLocalFineTuneInput,
|
|
59
|
+
validateLocalPipeline,
|
|
60
|
+
verifyLocalBaseModel,
|
|
61
|
+
withBundledPythonEnvironment,
|
|
62
|
+
withHuggingFaceCacheEnvironment,
|
|
63
|
+
withOfflineHuggingFaceCacheEnvironment,
|
|
64
|
+
writeJsonAtomic
|
|
65
|
+
} from "./chunk-DAP2Z7EZ.js";
|
|
66
|
+
|
|
67
|
+
// src/local-runtime/index.ts
|
|
68
|
+
import { readFileSync, realpathSync } from "fs";
|
|
69
|
+
import { createHash } from "crypto";
|
|
70
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
71
|
+
import { basename, dirname, join as join3, resolve as resolve3 } from "path";
|
|
72
|
+
import { cwd } from "process";
|
|
73
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
74
|
+
|
|
75
|
+
// src/local-runtime/compare.ts
|
|
76
|
+
function byPrompt(report) {
|
|
77
|
+
return new Map(report.results.map((result) => [result.prompt, result]));
|
|
78
|
+
}
|
|
79
|
+
function avg(values) {
|
|
80
|
+
return values.length > 0 ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
|
|
81
|
+
}
|
|
82
|
+
function summarizeRun(report) {
|
|
83
|
+
return {
|
|
84
|
+
run_id: report.run_id,
|
|
85
|
+
started_at: report.run_metadata.started_at,
|
|
86
|
+
base_model: report.base_model,
|
|
87
|
+
training_example_count: report.run_metadata.training_example_count,
|
|
88
|
+
eval_examples_used: report.run_metadata.eval_examples_used,
|
|
89
|
+
baseline_avg_score: report.baseline.avg_score,
|
|
90
|
+
candidate_avg_score: report.candidate.avg_score,
|
|
91
|
+
candidate_avg_token_f1: report.candidate.avg_token_f1 ?? null,
|
|
92
|
+
avg_score_delta: report.comparison.avg_score_delta
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function subsetSide(prompts, baseline, candidate) {
|
|
96
|
+
const candidateResults = prompts.map((prompt) => candidate.get(prompt)).filter((result) => Boolean(result));
|
|
97
|
+
return {
|
|
98
|
+
baseline_avg_score: avg(prompts.map((prompt) => baseline.get(prompt)?.score).filter((score) => typeof score === "number")),
|
|
99
|
+
candidate_avg_score: avg(candidateResults.map((result) => result.score)),
|
|
100
|
+
candidate_avg_token_f1: avg(candidateResults.map((result) => tokenF1(result.expected, result.actual)))
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function compareRuns(a, b) {
|
|
104
|
+
const aCandidate = byPrompt(a.candidate);
|
|
105
|
+
const bCandidate = byPrompt(b.candidate);
|
|
106
|
+
const aBaseline = byPrompt(a.baseline);
|
|
107
|
+
const bBaseline = byPrompt(b.baseline);
|
|
108
|
+
const sharedPrompts = [...bCandidate.keys()].filter((prompt) => aCandidate.has(prompt));
|
|
109
|
+
const bOnlyPrompts = [...bCandidate.keys()].filter((prompt) => !aCandidate.has(prompt));
|
|
110
|
+
const sharedA = subsetSide(sharedPrompts, aBaseline, aCandidate);
|
|
111
|
+
const sharedB = subsetSide(sharedPrompts, bBaseline, bCandidate);
|
|
112
|
+
const runA = summarizeRun(a);
|
|
113
|
+
const runB = summarizeRun(b);
|
|
114
|
+
const notes = [];
|
|
115
|
+
if (bOnlyPrompts.length > 0) {
|
|
116
|
+
notes.push(
|
|
117
|
+
`run_b evaluated ${bOnlyPrompts.length} example(s) run_a never saw; headline avg_score_delta values are not directly comparable across the two runs. Use the shared subset for recipe comparisons.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
run_a: runA,
|
|
122
|
+
run_b: runB,
|
|
123
|
+
shared: {
|
|
124
|
+
examples: sharedPrompts.length,
|
|
125
|
+
run_a: sharedA,
|
|
126
|
+
run_b: sharedB,
|
|
127
|
+
candidate_avg_score_delta: sharedA.candidate_avg_score !== null && sharedB.candidate_avg_score !== null ? sharedB.candidate_avg_score - sharedA.candidate_avg_score : null,
|
|
128
|
+
candidate_avg_token_f1_delta: sharedA.candidate_avg_token_f1 !== null && sharedB.candidate_avg_token_f1 !== null ? sharedB.candidate_avg_token_f1 - sharedA.candidate_avg_token_f1 : null
|
|
129
|
+
},
|
|
130
|
+
b_only: { examples: bOnlyPrompts.length, ...subsetSide(bOnlyPrompts, bBaseline, bCandidate) },
|
|
131
|
+
notes
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/local-runtime/doctor.ts
|
|
136
|
+
import { constants } from "fs";
|
|
137
|
+
import { access, mkdir, statfs, unlink, writeFile } from "fs/promises";
|
|
138
|
+
import { homedir } from "os";
|
|
139
|
+
import { join, resolve } from "path";
|
|
140
|
+
import { spawn } from "child_process";
|
|
141
|
+
async function runCommand(command, args, options = {}) {
|
|
142
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
143
|
+
let interrupted;
|
|
144
|
+
const result = await new Promise((resolveResult) => {
|
|
145
|
+
let settled = false;
|
|
146
|
+
let stdout = "";
|
|
147
|
+
let stderr = "";
|
|
148
|
+
let stopError;
|
|
149
|
+
let forceKillTimer = null;
|
|
150
|
+
const child = spawn(command, args, {
|
|
151
|
+
cwd: options.cwd,
|
|
152
|
+
env: options.env ?? process.env,
|
|
153
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
154
|
+
detached: process.platform !== "win32"
|
|
155
|
+
});
|
|
156
|
+
const killProcessGroup = (signal) => {
|
|
157
|
+
if (child.pid && process.platform !== "win32") {
|
|
158
|
+
try {
|
|
159
|
+
process.kill(-child.pid, signal);
|
|
160
|
+
return;
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
child.kill(signal);
|
|
165
|
+
};
|
|
166
|
+
const requestStop = (signal = "SIGTERM") => {
|
|
167
|
+
killProcessGroup(signal);
|
|
168
|
+
if (!forceKillTimer) {
|
|
169
|
+
forceKillTimer = setTimeout(() => killProcessGroup("SIGKILL"), 5e3);
|
|
170
|
+
forceKillTimer.unref();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const onSigint = () => {
|
|
174
|
+
interrupted = "interrupted by SIGINT";
|
|
175
|
+
stopError = interrupted;
|
|
176
|
+
requestStop("SIGINT");
|
|
177
|
+
};
|
|
178
|
+
const onSigterm = () => {
|
|
179
|
+
interrupted = "interrupted by SIGTERM";
|
|
180
|
+
stopError = interrupted;
|
|
181
|
+
requestStop("SIGTERM");
|
|
182
|
+
};
|
|
183
|
+
const finish = (result2) => {
|
|
184
|
+
if (settled) return;
|
|
185
|
+
settled = true;
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
188
|
+
process.off("SIGINT", onSigint);
|
|
189
|
+
process.off("SIGTERM", onSigterm);
|
|
190
|
+
resolveResult(result2);
|
|
191
|
+
};
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
stopError = `timed out after ${timeoutMs}ms`;
|
|
194
|
+
requestStop();
|
|
195
|
+
}, timeoutMs);
|
|
196
|
+
timer.unref();
|
|
197
|
+
process.once("SIGINT", onSigint);
|
|
198
|
+
process.once("SIGTERM", onSigterm);
|
|
199
|
+
child.stdout.setEncoding("utf8");
|
|
200
|
+
child.stderr.setEncoding("utf8");
|
|
201
|
+
child.stdout.on("data", (chunk) => {
|
|
202
|
+
stdout += chunk;
|
|
203
|
+
});
|
|
204
|
+
child.stderr.on("data", (chunk) => {
|
|
205
|
+
stderr += chunk;
|
|
206
|
+
});
|
|
207
|
+
child.on("error", (error) => {
|
|
208
|
+
finish({ code: null, stdout, stderr, error: error.message });
|
|
209
|
+
});
|
|
210
|
+
child.on("close", (code) => {
|
|
211
|
+
finish({ code: stopError ? null : code, stdout, stderr, error: stopError });
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
if (interrupted) throw new Error(interrupted);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
function firstLine(value) {
|
|
218
|
+
return value.trim().split(/\r?\n/)[0] ?? "";
|
|
219
|
+
}
|
|
220
|
+
function commandText(command, args) {
|
|
221
|
+
return [command, ...args].map((part) => /\s/.test(part) ? JSON.stringify(part) : part).join(" ");
|
|
222
|
+
}
|
|
223
|
+
async function writableDirectoryCheck(name, path) {
|
|
224
|
+
const resolvedPath = resolve(path);
|
|
225
|
+
const probePath = join(resolvedPath, `.tt-local-write-probe-${process.pid}-${Date.now()}`);
|
|
226
|
+
try {
|
|
227
|
+
await mkdir(resolvedPath, { recursive: true });
|
|
228
|
+
await access(resolvedPath, constants.R_OK | constants.W_OK);
|
|
229
|
+
await writeFile(probePath, "ok\n", { flag: "wx" });
|
|
230
|
+
await unlink(probePath);
|
|
231
|
+
const fs = await statfs(resolvedPath);
|
|
232
|
+
const freeBytes = Number(fs.bavail) * Number(fs.bsize);
|
|
233
|
+
return {
|
|
234
|
+
name,
|
|
235
|
+
ok: true,
|
|
236
|
+
message: `${resolvedPath} is writable (${Math.round(freeBytes / 1024 ** 3 * 10) / 10} GiB available)`,
|
|
237
|
+
details: { path: resolvedPath, free_bytes: freeBytes }
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
await unlink(probePath).catch(() => void 0);
|
|
241
|
+
return {
|
|
242
|
+
name,
|
|
243
|
+
ok: false,
|
|
244
|
+
message: `${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
245
|
+
details: { path: resolvedPath }
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function pythonProbeSource(device) {
|
|
250
|
+
return [
|
|
251
|
+
"import json",
|
|
252
|
+
"import torch, transformers, peft, huggingface_hub",
|
|
253
|
+
`requested = ${JSON.stringify(device)}`,
|
|
254
|
+
"assert torch.cuda.is_available(), 'TT Local training requires CUDA but torch.cuda.is_available() is false'",
|
|
255
|
+
"props = torch.cuda.get_device_properties(0)",
|
|
256
|
+
"print(json.dumps({'python_ok': True, 'torch': torch.__version__, 'transformers': transformers.__version__, 'cuda_available': True, 'cuda_device': torch.cuda.get_device_name(0), 'compute_capability': list(torch.cuda.get_device_capability(0)), 'total_memory_bytes': props.total_memory, 'evaluation_device': requested}))"
|
|
257
|
+
].join("; ");
|
|
258
|
+
}
|
|
259
|
+
function pythonProbePlan(args) {
|
|
260
|
+
const entrypoint = buildBundledPythonCommand(
|
|
261
|
+
"-c",
|
|
262
|
+
[pythonProbeSource(args.device)]
|
|
263
|
+
);
|
|
264
|
+
const env = withBundledPythonEnvironment(
|
|
265
|
+
withHuggingFaceCacheEnvironment(
|
|
266
|
+
minimalMachineLearningEnvironment(process.env),
|
|
267
|
+
args.modelCache
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
return {
|
|
271
|
+
name: "python-runtime",
|
|
272
|
+
command: entrypoint.command,
|
|
273
|
+
args: entrypoint.commandArgs,
|
|
274
|
+
env
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function buildDoctorPythonPlans(config) {
|
|
278
|
+
if (config.dryRun) return [];
|
|
279
|
+
return [
|
|
280
|
+
pythonProbePlan({
|
|
281
|
+
device: config.evaluation.inference.device,
|
|
282
|
+
modelCache: config.paths.modelCache
|
|
283
|
+
})
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
function placeholderSpecCheck(request) {
|
|
287
|
+
const placeholder = request.spec_snapshot.examples.some(
|
|
288
|
+
(example) => /replace this with/i.test(example.input) || /replace this with/i.test(example.output)
|
|
289
|
+
) || /describe the behavior this local model should learn/i.test(request.spec_snapshot.system_prompt);
|
|
290
|
+
return {
|
|
291
|
+
name: "spec-content",
|
|
292
|
+
ok: !placeholder,
|
|
293
|
+
message: placeholder ? "The spec still contains generated placeholder content; edit it before training." : `${request.spec_snapshot.examples.length} spec example(s); base model ${request.spec_snapshot.base_model}`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
async function localBaseModelCheck(path) {
|
|
297
|
+
const resolvedPath = resolve(path);
|
|
298
|
+
try {
|
|
299
|
+
const verified = await verifyLocalBaseModel(resolvedPath);
|
|
300
|
+
return {
|
|
301
|
+
name: "local-base-model",
|
|
302
|
+
ok: true,
|
|
303
|
+
message: `${resolvedPath} is a valid local Hugging Face snapshot directory.`,
|
|
304
|
+
details: { path: resolvedPath, file_count: verified.fileCount, size_bytes: verified.sizeBytes }
|
|
305
|
+
};
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return {
|
|
308
|
+
name: "local-base-model",
|
|
309
|
+
ok: false,
|
|
310
|
+
message: error instanceof Error ? error.message : String(error),
|
|
311
|
+
details: { path: resolvedPath }
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function runDoctor(config, request) {
|
|
316
|
+
const checks = [];
|
|
317
|
+
const nodeVersion = process.versions.node;
|
|
318
|
+
const nodeMajor = Number(nodeVersion.split(".")[0]);
|
|
319
|
+
checks.push({ name: "node", ok: nodeMajor >= 22, message: `Node ${nodeVersion}` });
|
|
320
|
+
checks.push(await writableDirectoryCheck("artifact-root", config.artifactRoot));
|
|
321
|
+
checks.push(await writableDirectoryCheck("store-root", config.storeRoot ?? defaultLocalHome()));
|
|
322
|
+
checks.push(await writableDirectoryCheck(
|
|
323
|
+
"model-cache",
|
|
324
|
+
config.paths.modelCache ?? process.env.HF_HOME ?? join(homedir(), ".cache", "huggingface")
|
|
325
|
+
));
|
|
326
|
+
if (config.paths.baseModel) checks.push(await localBaseModelCheck(config.paths.baseModel));
|
|
327
|
+
if (request) {
|
|
328
|
+
checks.push(placeholderSpecCheck(request));
|
|
329
|
+
resolveTrainingModel(request.spec_snapshot.base_model);
|
|
330
|
+
}
|
|
331
|
+
const pythonPlans = buildDoctorPythonPlans(config);
|
|
332
|
+
if (pythonPlans.length > 0) {
|
|
333
|
+
const uvVersion = await runCommand("uv", ["--version"]);
|
|
334
|
+
checks.push({
|
|
335
|
+
name: "uv",
|
|
336
|
+
ok: uvVersion.code === 0,
|
|
337
|
+
message: uvVersion.code === 0 ? firstLine(uvVersion.stdout) : uvVersion.error ?? (firstLine(uvVersion.stderr) || "uv is not available")
|
|
338
|
+
});
|
|
339
|
+
if (uvVersion.code === 0) {
|
|
340
|
+
const uniquePlans = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const plan of pythonPlans) {
|
|
342
|
+
const key = JSON.stringify([plan.command, plan.args, plan.env]);
|
|
343
|
+
if (!uniquePlans.has(key)) uniquePlans.set(key, plan);
|
|
344
|
+
}
|
|
345
|
+
for (const plan of uniquePlans.values()) {
|
|
346
|
+
const result = await runCommand(plan.command, plan.args, {
|
|
347
|
+
env: plan.env,
|
|
348
|
+
timeoutMs: 18e5
|
|
349
|
+
});
|
|
350
|
+
checks.push({
|
|
351
|
+
name: plan.name,
|
|
352
|
+
ok: result.code === 0,
|
|
353
|
+
message: result.code === 0 ? firstLine(result.stdout) : result.error ?? (firstLine(result.stderr) || `${plan.command} exited ${result.code}`),
|
|
354
|
+
details: { command: commandText(plan.command, plan.args) }
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
checks.push({
|
|
360
|
+
name: "python-runtime",
|
|
361
|
+
ok: true,
|
|
362
|
+
message: config.dryRun ? "Python dependency checks skipped because dryRun is enabled." : "Python dependency checks were skipped."
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const device = config.evaluation.inference.device;
|
|
366
|
+
if (!config.dryRun) {
|
|
367
|
+
const nvidiaSmi = await runCommand("nvidia-smi", []);
|
|
368
|
+
checks.push({
|
|
369
|
+
name: "nvidia-smi",
|
|
370
|
+
ok: nvidiaSmi.code === 0,
|
|
371
|
+
message: nvidiaSmi.code === 0 ? firstLine(nvidiaSmi.stdout) : nvidiaSmi.error ?? (firstLine(nvidiaSmi.stderr) || "nvidia-smi not available")
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
checks.push({
|
|
375
|
+
name: "nvidia-smi",
|
|
376
|
+
ok: true,
|
|
377
|
+
message: config.dryRun ? "GPU checks skipped because dryRun is enabled." : `nvidia-smi is not required for device=${device}.`
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const trainingCommand = buildBundledPythonCommand("train.py").displayCommand;
|
|
381
|
+
checks.push({
|
|
382
|
+
name: "effective-plan",
|
|
383
|
+
ok: true,
|
|
384
|
+
message: "Resolved the configured training and evaluation plan.",
|
|
385
|
+
details: {
|
|
386
|
+
training_command: trainingCommand ?? null,
|
|
387
|
+
evaluation_provider: "transformers",
|
|
388
|
+
evaluation_device: device,
|
|
389
|
+
artifact_root: resolve(config.artifactRoot),
|
|
390
|
+
store_root: resolve(config.storeRoot ?? defaultLocalHome()),
|
|
391
|
+
model_cache: resolve(config.paths.modelCache ?? process.env.HF_HOME ?? join(homedir(), ".cache", "huggingface")),
|
|
392
|
+
base_model: request?.spec_snapshot.base_model ?? null,
|
|
393
|
+
scoring_mode: config.evaluation.scoring.mode
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
return checks;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/local-runtime/model-server.ts
|
|
400
|
+
import { spawn as spawn2 } from "child_process";
|
|
401
|
+
import { resolve as resolve2 } from "path";
|
|
402
|
+
import { fileURLToPath } from "url";
|
|
403
|
+
function localArtifactPath(uri) {
|
|
404
|
+
if (uri.startsWith("file://")) return fileURLToPath(uri);
|
|
405
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(uri)) {
|
|
406
|
+
throw new Error(`TT Local serving requires a local file artifact, got: ${uri}`);
|
|
407
|
+
}
|
|
408
|
+
return resolve2(uri);
|
|
409
|
+
}
|
|
410
|
+
function boundedNumber(name, value, minimum, maximum, integer = false) {
|
|
411
|
+
if (!Number.isFinite(value) || value < minimum || value > maximum || integer && !Number.isInteger(value)) {
|
|
412
|
+
throw new Error(`${name} must be between ${minimum} and ${maximum}`);
|
|
413
|
+
}
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
function isLoopbackHost(host) {
|
|
417
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
|
|
418
|
+
}
|
|
419
|
+
function httpHost(host) {
|
|
420
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
421
|
+
}
|
|
422
|
+
function buildModelServerLaunch(args) {
|
|
423
|
+
const options = args.options ?? {};
|
|
424
|
+
const host = options.host ?? "127.0.0.1";
|
|
425
|
+
const remote = !isLoopbackHost(host);
|
|
426
|
+
if (remote && !options.allowRemote) {
|
|
427
|
+
throw new Error("Refusing a non-loopback model server bind without --allow-remote.");
|
|
428
|
+
}
|
|
429
|
+
const apiKey = options.apiKeyEnv ? process.env[options.apiKeyEnv]?.trim() : void 0;
|
|
430
|
+
if (options.apiKeyEnv && !apiKey) throw new Error(`${options.apiKeyEnv} is not set.`);
|
|
431
|
+
if (remote && !apiKey) {
|
|
432
|
+
throw new Error("Non-loopback serving requires --api-key-env with a populated environment variable.");
|
|
433
|
+
}
|
|
434
|
+
const port = boundedNumber("port", options.port ?? 8e3, 1, 65535, true);
|
|
435
|
+
const maxTokens = boundedNumber("maxTokens", options.maxTokens ?? 512, 1, 8192, true);
|
|
436
|
+
const temperature = boundedNumber("temperature", options.temperature ?? 0, 0, 5);
|
|
437
|
+
const topP = boundedNumber("topP", options.topP ?? 1, 0, 1);
|
|
438
|
+
const maxConcurrentRequests = boundedNumber(
|
|
439
|
+
"maxConcurrentRequests",
|
|
440
|
+
options.maxConcurrentRequests ?? 1,
|
|
441
|
+
1,
|
|
442
|
+
8,
|
|
443
|
+
true
|
|
444
|
+
);
|
|
445
|
+
const artifactPath = args.artifactUri ? localArtifactPath(args.artifactUri) : void 0;
|
|
446
|
+
const recordedBaseModelPath = options.baseModelArtifactUri ? localArtifactPath(options.baseModelArtifactUri) : void 0;
|
|
447
|
+
const configuredBaseModelPath = args.config.paths.baseModel ? resolve2(args.config.paths.baseModel) : void 0;
|
|
448
|
+
if (recordedBaseModelPath && configuredBaseModelPath && resolve2(recordedBaseModelPath) !== configuredBaseModelPath) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Configured paths.baseModel ${configuredBaseModelPath} does not match the base model recorded for this artifact: ` + recordedBaseModelPath
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const localBaseModelPath = recordedBaseModelPath ?? configuredBaseModelPath;
|
|
454
|
+
resolveTrainingModel(args.baseModel);
|
|
455
|
+
const entrypoint = buildBundledPythonCommand("serve.py");
|
|
456
|
+
const env = withBundledPythonEnvironment(
|
|
457
|
+
withOfflineHuggingFaceCacheEnvironment({
|
|
458
|
+
...minimalMachineLearningEnvironment(process.env),
|
|
459
|
+
...artifactPath ? { TT_MODEL_ARTIFACT: artifactPath } : {},
|
|
460
|
+
TT_BASE_MODEL: localBaseModelPath ?? args.baseModel,
|
|
461
|
+
...options.baseModelRevision && !localBaseModelPath ? { TT_BASE_MODEL_REVISION: options.baseModelRevision } : {},
|
|
462
|
+
TT_MODEL_NAME: args.modelName,
|
|
463
|
+
TT_MODEL_LOADER: "causal_lm",
|
|
464
|
+
TT_HOST: host,
|
|
465
|
+
TT_PORT: String(port),
|
|
466
|
+
TT_DEVICE: options.device ?? args.config.evaluation.inference.device,
|
|
467
|
+
TT_MAX_TOKENS: String(maxTokens),
|
|
468
|
+
TT_TEMPERATURE: String(temperature),
|
|
469
|
+
TT_TOP_P: String(topP),
|
|
470
|
+
TT_TRUST_REMOTE_CODE: "false",
|
|
471
|
+
TT_MAX_CONCURRENT_REQUESTS: String(maxConcurrentRequests),
|
|
472
|
+
...options.systemPrompt ? { TT_SYSTEM_PROMPT: options.systemPrompt } : {},
|
|
473
|
+
...apiKey ? { TT_API_KEY: apiKey } : {}
|
|
474
|
+
}, args.config.paths.modelCache)
|
|
475
|
+
);
|
|
476
|
+
return {
|
|
477
|
+
command: entrypoint.command,
|
|
478
|
+
commandArgs: entrypoint.commandArgs,
|
|
479
|
+
displayCommand: entrypoint.displayCommand,
|
|
480
|
+
env,
|
|
481
|
+
url: `http://${httpHost(host)}:${port}`,
|
|
482
|
+
modelName: args.modelName,
|
|
483
|
+
artifactPath
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function buildLocalModelServerLaunch(args) {
|
|
487
|
+
return buildModelServerLaunch({
|
|
488
|
+
modelName: args.model.id,
|
|
489
|
+
baseModel: args.model.base_model,
|
|
490
|
+
artifactUri: args.model.artifact_uri,
|
|
491
|
+
config: args.config,
|
|
492
|
+
options: args.options
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function buildLocalBaseModelServerLaunch(args) {
|
|
496
|
+
return buildModelServerLaunch({
|
|
497
|
+
modelName: `base:${args.baseModel}`,
|
|
498
|
+
baseModel: args.baseModel,
|
|
499
|
+
config: args.config,
|
|
500
|
+
options: args.options
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
async function serveLocalModel(launch) {
|
|
504
|
+
await new Promise((resolveServer, reject) => {
|
|
505
|
+
const child = spawn2(launch.command, launch.commandArgs, {
|
|
506
|
+
env: launch.env,
|
|
507
|
+
stdio: "inherit",
|
|
508
|
+
detached: process.platform !== "win32"
|
|
509
|
+
});
|
|
510
|
+
let stopping = false;
|
|
511
|
+
let forceKillTimer;
|
|
512
|
+
const killProcessGroup = (signal) => {
|
|
513
|
+
if (child.pid && process.platform !== "win32") {
|
|
514
|
+
try {
|
|
515
|
+
process.kill(-child.pid, signal);
|
|
516
|
+
return;
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
child.kill(signal);
|
|
521
|
+
};
|
|
522
|
+
const stop = (signal) => {
|
|
523
|
+
if (stopping) return;
|
|
524
|
+
stopping = true;
|
|
525
|
+
killProcessGroup(signal);
|
|
526
|
+
forceKillTimer = setTimeout(() => killProcessGroup("SIGKILL"), 5e3);
|
|
527
|
+
forceKillTimer.unref();
|
|
528
|
+
};
|
|
529
|
+
const onSigint = () => stop("SIGINT");
|
|
530
|
+
const onSigterm = () => stop("SIGTERM");
|
|
531
|
+
const cleanup = () => {
|
|
532
|
+
process.off("SIGINT", onSigint);
|
|
533
|
+
process.off("SIGTERM", onSigterm);
|
|
534
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
535
|
+
};
|
|
536
|
+
process.once("SIGINT", onSigint);
|
|
537
|
+
process.once("SIGTERM", onSigterm);
|
|
538
|
+
child.once("error", (error) => {
|
|
539
|
+
cleanup();
|
|
540
|
+
reject(error);
|
|
541
|
+
});
|
|
542
|
+
child.once("close", (code, signal) => {
|
|
543
|
+
cleanup();
|
|
544
|
+
if (stopping || signal === "SIGINT" || signal === "SIGTERM") {
|
|
545
|
+
resolveServer();
|
|
546
|
+
} else if (code === 0) {
|
|
547
|
+
resolveServer();
|
|
548
|
+
} else {
|
|
549
|
+
reject(new Error(`Local model server exited with code ${code ?? "unknown"}.`));
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/local-runtime/active-model.ts
|
|
556
|
+
import { appendFile, readFile } from "fs/promises";
|
|
557
|
+
import { join as join2 } from "path";
|
|
558
|
+
function pointerPath(store) {
|
|
559
|
+
return join2(store.root, "active-model.json");
|
|
560
|
+
}
|
|
561
|
+
function historyPath(store) {
|
|
562
|
+
return join2(store.root, "activation-history.jsonl");
|
|
563
|
+
}
|
|
564
|
+
async function readPointer(store) {
|
|
565
|
+
try {
|
|
566
|
+
const value = JSON.parse(await readFile(pointerPath(store), "utf8"));
|
|
567
|
+
if (value.schema_version !== 1 || !["activate", "rollback"].includes(value.action) || value.model_id !== null && typeof value.model_id !== "string" || value.run_id !== null && typeof value.run_id !== "string" || value.previous_model_id !== null && typeof value.previous_model_id !== "string") {
|
|
568
|
+
throw new Error("Active model pointer has an invalid shape.");
|
|
569
|
+
}
|
|
570
|
+
return value;
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (error.code === "ENOENT") return null;
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function assertModelCanActivate(store, modelId) {
|
|
577
|
+
const model = await store.getModel(modelId);
|
|
578
|
+
const run = await store.getRun(model.run_id);
|
|
579
|
+
if (run.status !== "completed") {
|
|
580
|
+
throw new Error(`Model ${model.id} belongs to run ${run.id}, which is not completed.`);
|
|
581
|
+
}
|
|
582
|
+
const report = runReportSchema.parse(
|
|
583
|
+
JSON.parse(await readFile(join2(model.artifact_dir, "run-report.json"), "utf8"))
|
|
584
|
+
);
|
|
585
|
+
if (!report.general_regression) {
|
|
586
|
+
throw new Error(`Model ${model.id} has no general regression evaluation.`);
|
|
587
|
+
}
|
|
588
|
+
if (!report.general_regression.passed) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Model ${model.id} failed general regression: ` + (report.general_regression.failures.join(" ") || "regression budget exceeded")
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
return model;
|
|
594
|
+
}
|
|
595
|
+
async function publishPointer(store, pointer) {
|
|
596
|
+
await store.ensure();
|
|
597
|
+
await writeJsonAtomic(pointerPath(store), pointer);
|
|
598
|
+
await appendFile(historyPath(store), `${JSON.stringify(pointer)}
|
|
599
|
+
`, "utf8");
|
|
600
|
+
return pointer;
|
|
601
|
+
}
|
|
602
|
+
async function getActiveModel(store) {
|
|
603
|
+
const pointer = await readPointer(store);
|
|
604
|
+
if (!pointer?.model_id) return { pointer, model: null };
|
|
605
|
+
return { pointer, model: await store.getModel(pointer.model_id) };
|
|
606
|
+
}
|
|
607
|
+
async function activateModel(store, modelId) {
|
|
608
|
+
const [model, current] = await Promise.all([
|
|
609
|
+
assertModelCanActivate(store, modelId),
|
|
610
|
+
readPointer(store)
|
|
611
|
+
]);
|
|
612
|
+
if (current?.model_id === model.id) return current;
|
|
613
|
+
return publishPointer(store, {
|
|
614
|
+
schema_version: 1,
|
|
615
|
+
model_id: model.id,
|
|
616
|
+
run_id: model.run_id,
|
|
617
|
+
previous_model_id: current?.model_id ?? null,
|
|
618
|
+
activated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
619
|
+
action: "activate"
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
async function rollbackActiveModel(store) {
|
|
623
|
+
const current = await readPointer(store);
|
|
624
|
+
if (!current) throw new Error("No active-model history exists to roll back.");
|
|
625
|
+
const targetId = current.previous_model_id;
|
|
626
|
+
const target = targetId ? await assertModelCanActivate(store, targetId) : null;
|
|
627
|
+
return publishPointer(store, {
|
|
628
|
+
schema_version: 1,
|
|
629
|
+
model_id: target?.id ?? null,
|
|
630
|
+
run_id: target?.run_id ?? null,
|
|
631
|
+
previous_model_id: current.model_id,
|
|
632
|
+
activated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
633
|
+
action: "rollback"
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/local-runtime/index.ts
|
|
638
|
+
function packageVersion() {
|
|
639
|
+
try {
|
|
640
|
+
const packageJson = JSON.parse(
|
|
641
|
+
readFileSync(resolve3(localRuntimePackageRoot(import.meta.url), "package.json"), "utf8")
|
|
642
|
+
);
|
|
643
|
+
return typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
644
|
+
} catch {
|
|
645
|
+
return "unknown";
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
var TT_LOCAL_VERSION = packageVersion();
|
|
649
|
+
function getLocalRunnerInfo() {
|
|
650
|
+
return {
|
|
651
|
+
name: "tuned-tensor-local",
|
|
652
|
+
status: "local-runner-preview",
|
|
653
|
+
description: "Local CUDA LoRA fine-tuning with held-out base-versus-tuned evaluation.",
|
|
654
|
+
version: TT_LOCAL_VERSION
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function readOption(argv, name) {
|
|
658
|
+
const index = argv.indexOf(name);
|
|
659
|
+
if (index !== -1) return argv[index + 1];
|
|
660
|
+
const inline = argv.find((value) => value.startsWith(`${name}=`));
|
|
661
|
+
return inline?.slice(name.length + 1);
|
|
662
|
+
}
|
|
663
|
+
function hasFlag(argv, name) {
|
|
664
|
+
return argv.includes(name);
|
|
665
|
+
}
|
|
666
|
+
function printHelp() {
|
|
667
|
+
console.log(`Usage: tt-local <command> [options]
|
|
668
|
+
|
|
669
|
+
Commands:
|
|
670
|
+
info Show package and runner information
|
|
671
|
+
init [--name "My Local Model"] [--model Qwen/Qwen3.5-2B] [--output tunedtensor.json] [--profile spark] [--force]
|
|
672
|
+
doctor [tunedtensor.json] [--config local-runner.json]
|
|
673
|
+
validate [tunedtensor.json] [--config local-runner.json]
|
|
674
|
+
run [tunedtensor.json] [--config local-runner.json] [--dry-run] [--verbose] [--quiet]
|
|
675
|
+
serve <model-id|active|base> [--config local-runner.json] [--host 127.0.0.1] [--port 8000]
|
|
676
|
+
runs list|get|events|report|compare [args] [--config local-runner.json]
|
|
677
|
+
models list|get|verify|prefetch|verify-base|active|activate|rollback|serve [args] [--config local-runner.json]
|
|
678
|
+
|
|
679
|
+
Global options:
|
|
680
|
+
-h, --help Show help
|
|
681
|
+
-V, --version Show the installed version
|
|
682
|
+
|
|
683
|
+
The run command writes local artifacts under config.artifactRoot, defaulting to
|
|
684
|
+
.tt-local/artifacts. The file-backed local store defaults to
|
|
685
|
+
~/.tuned-tensor-local unless config.storeRoot or TT_LOCAL_HOME is set.`);
|
|
686
|
+
}
|
|
687
|
+
var CONFIG_OPTION = { name: "--config", value: "path", description: "Local runner config JSON path" };
|
|
688
|
+
var VERBOSE_OPTION = { name: "--verbose", description: "Stream subprocess output" };
|
|
689
|
+
var QUIET_OPTION = { name: "--quiet", description: "Suppress progress output on stderr" };
|
|
690
|
+
var MODEL_SERVE_OPTIONS = [
|
|
691
|
+
CONFIG_OPTION,
|
|
692
|
+
{ name: "--host", value: "host", description: "Bind host (localhost by default)" },
|
|
693
|
+
{ name: "--port", value: "port", description: "Bind port" },
|
|
694
|
+
{ name: "--device", value: "device", description: "cuda or cpu" },
|
|
695
|
+
{ name: "--max-tokens", value: "count", description: "Default response token limit" },
|
|
696
|
+
{ name: "--temperature", value: "number", description: "Default sampling temperature" },
|
|
697
|
+
{ name: "--top-p", value: "number", description: "Default nucleus sampling threshold" },
|
|
698
|
+
{ name: "--max-concurrent-requests", value: "count", description: "Concurrent generation limit" },
|
|
699
|
+
{ name: "--spec", value: "path", description: "Behavior spec whose instructions are enforced" },
|
|
700
|
+
{ name: "--no-spec-prompt", description: "Do not enforce the stored behavior-spec prompt" },
|
|
701
|
+
{ name: "--allow-remote", description: "Allow a non-loopback bind" },
|
|
702
|
+
{ name: "--api-key-env", value: "name", description: "Environment variable containing a bearer token" },
|
|
703
|
+
{ name: "--print-command", description: "Validate and print the launch plan without starting" }
|
|
704
|
+
];
|
|
705
|
+
var COMMAND_DEFINITIONS = {
|
|
706
|
+
info: {
|
|
707
|
+
usage: "tt-local info",
|
|
708
|
+
description: "Show the installed TT Local version and runner status.",
|
|
709
|
+
options: [],
|
|
710
|
+
maxPositionals: 0
|
|
711
|
+
},
|
|
712
|
+
init: {
|
|
713
|
+
usage: "tt-local init [options]",
|
|
714
|
+
description: "Create a local tunedtensor.json behavior spec.",
|
|
715
|
+
options: [
|
|
716
|
+
{ name: "--name", value: "name", description: "Behavior spec name" },
|
|
717
|
+
{ name: "--model", value: "model", description: "Base model ID" },
|
|
718
|
+
{ name: "--output", value: "path", description: "Output spec path" },
|
|
719
|
+
{ name: "--profile", value: "profile", description: "Write a durable runner config (spark)" },
|
|
720
|
+
{ name: "--config", value: "path", description: "Runner config path (written with --profile)" },
|
|
721
|
+
{ name: "--force", description: "Overwrite an existing output file" }
|
|
722
|
+
],
|
|
723
|
+
maxPositionals: 0
|
|
724
|
+
},
|
|
725
|
+
doctor: {
|
|
726
|
+
usage: "tt-local doctor [tunedtensor.json] [--config path]",
|
|
727
|
+
description: "Check the host and optional run input before starting work.",
|
|
728
|
+
options: [CONFIG_OPTION],
|
|
729
|
+
maxPositionals: 1
|
|
730
|
+
},
|
|
731
|
+
validate: {
|
|
732
|
+
usage: "tt-local validate [tunedtensor.json] [options]",
|
|
733
|
+
description: "Validate a local behavior spec without executing it.",
|
|
734
|
+
options: [CONFIG_OPTION],
|
|
735
|
+
maxPositionals: 1
|
|
736
|
+
},
|
|
737
|
+
run: {
|
|
738
|
+
usage: "tt-local run [tunedtensor.json] [options]",
|
|
739
|
+
description: "Run the baseline, fine-tuning, tuned evaluation, and report workflow.",
|
|
740
|
+
options: [
|
|
741
|
+
CONFIG_OPTION,
|
|
742
|
+
{ name: "--dry-run", description: "Write representative artifacts without GPU work" },
|
|
743
|
+
VERBOSE_OPTION,
|
|
744
|
+
QUIET_OPTION
|
|
745
|
+
],
|
|
746
|
+
maxPositionals: 1
|
|
747
|
+
},
|
|
748
|
+
serve: {
|
|
749
|
+
usage: "tt-local serve <model-id|active|base> [options]",
|
|
750
|
+
description: "Serve a verified adapter, the active model, or the protected base.",
|
|
751
|
+
options: MODEL_SERVE_OPTIONS,
|
|
752
|
+
minPositionals: 1,
|
|
753
|
+
maxPositionals: 1,
|
|
754
|
+
missingPositionalsMessage: "serve requires <model-id>"
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
var COMMAND_GROUPS = {
|
|
758
|
+
runs: {
|
|
759
|
+
description: "Inspect locally stored runs.",
|
|
760
|
+
defaultSubcommand: "list",
|
|
761
|
+
subcommands: {
|
|
762
|
+
list: { usage: "tt-local runs list [--config path]", description: "List local runs.", options: [CONFIG_OPTION], maxPositionals: 0 },
|
|
763
|
+
get: { usage: "tt-local runs get <run-id> [--config path]", description: "Get a local run.", options: [CONFIG_OPTION], minPositionals: 1, maxPositionals: 1, missingPositionalsMessage: "runs get requires <run-id>" },
|
|
764
|
+
events: { usage: "tt-local runs events <run-id> [--config path]", description: "List run events.", options: [CONFIG_OPTION], minPositionals: 1, maxPositionals: 1, missingPositionalsMessage: "runs events requires <run-id>" },
|
|
765
|
+
report: { usage: "tt-local runs report <run-id> [--config path]", description: "Show the baseline-vs-tuned report, including deltas and regressions.", options: [CONFIG_OPTION], minPositionals: 1, maxPositionals: 1, missingPositionalsMessage: "runs report requires <run-id>" },
|
|
766
|
+
compare: { usage: "tt-local runs compare <run-id-a> <run-id-b> [--config path]", description: "Compare two run reports.", options: [CONFIG_OPTION], minPositionals: 2, maxPositionals: 2, missingPositionalsMessage: "runs compare requires <run-id-a> <run-id-b>" }
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
models: {
|
|
770
|
+
description: "Inspect, verify, prefetch, or serve local models.",
|
|
771
|
+
defaultSubcommand: "list",
|
|
772
|
+
subcommands: {
|
|
773
|
+
list: { usage: "tt-local models list [--config path]", description: "List local models.", options: [CONFIG_OPTION], maxPositionals: 0 },
|
|
774
|
+
get: { usage: "tt-local models get <model-id> [--config path]", description: "Get a local model.", options: [CONFIG_OPTION], minPositionals: 1, maxPositionals: 1, missingPositionalsMessage: "models get requires <model-id>" },
|
|
775
|
+
verify: {
|
|
776
|
+
usage: "tt-local models verify <model-id-or-artifact-path> [--config path]",
|
|
777
|
+
description: "Verify a stored model or manifested artifact path.",
|
|
778
|
+
options: [CONFIG_OPTION],
|
|
779
|
+
minPositionals: 1,
|
|
780
|
+
maxPositionals: 1,
|
|
781
|
+
missingPositionalsMessage: "models verify requires <model-id-or-artifact-path>"
|
|
782
|
+
},
|
|
783
|
+
prefetch: {
|
|
784
|
+
usage: "tt-local models prefetch [tunedtensor.json] [options]",
|
|
785
|
+
description: "Download the configured base model before a run.",
|
|
786
|
+
options: [CONFIG_OPTION, VERBOSE_OPTION, QUIET_OPTION],
|
|
787
|
+
maxPositionals: 1
|
|
788
|
+
},
|
|
789
|
+
"verify-base": {
|
|
790
|
+
usage: "tt-local models verify-base [tunedtensor.json] [options]",
|
|
791
|
+
description: "Verify that the configured base-model snapshot is complete and locally available.",
|
|
792
|
+
options: [CONFIG_OPTION, VERBOSE_OPTION, QUIET_OPTION],
|
|
793
|
+
maxPositionals: 1
|
|
794
|
+
},
|
|
795
|
+
active: {
|
|
796
|
+
usage: "tt-local models active [--config path]",
|
|
797
|
+
description: "Show the active adapter, or the protected base when none is active.",
|
|
798
|
+
options: [CONFIG_OPTION],
|
|
799
|
+
maxPositionals: 0
|
|
800
|
+
},
|
|
801
|
+
activate: {
|
|
802
|
+
usage: "tt-local models activate <model-id> [--config path]",
|
|
803
|
+
description: "Activate a verified model whose general regression gate passed.",
|
|
804
|
+
options: [CONFIG_OPTION],
|
|
805
|
+
minPositionals: 1,
|
|
806
|
+
maxPositionals: 1,
|
|
807
|
+
missingPositionalsMessage: "models activate requires <model-id>"
|
|
808
|
+
},
|
|
809
|
+
rollback: {
|
|
810
|
+
usage: "tt-local models rollback [--config path]",
|
|
811
|
+
description: "Restore the previously active adapter or protected base.",
|
|
812
|
+
options: [CONFIG_OPTION],
|
|
813
|
+
maxPositionals: 0
|
|
814
|
+
},
|
|
815
|
+
serve: {
|
|
816
|
+
usage: "tt-local models serve <model-id|active|base> [options]",
|
|
817
|
+
description: "Alias for `tt-local serve`.",
|
|
818
|
+
options: MODEL_SERVE_OPTIONS,
|
|
819
|
+
minPositionals: 1,
|
|
820
|
+
maxPositionals: 1,
|
|
821
|
+
missingPositionalsMessage: "models serve requires <model-id>"
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
function hasHelpFlag(argv) {
|
|
827
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
828
|
+
}
|
|
829
|
+
function printCommandHelp(definition) {
|
|
830
|
+
console.log(`Usage: ${definition.usage}
|
|
831
|
+
|
|
832
|
+
${definition.description}`);
|
|
833
|
+
if (definition.options.length > 0) {
|
|
834
|
+
console.log("\nOptions:");
|
|
835
|
+
for (const option of definition.options) {
|
|
836
|
+
const label = option.value ? `${option.name} <${option.value}>` : option.name;
|
|
837
|
+
console.log(` ${label.padEnd(34)} ${option.description}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
console.log(" -h, --help Show help");
|
|
841
|
+
}
|
|
842
|
+
function printGroupHelp(command, group) {
|
|
843
|
+
console.log(`Usage: tt-local ${command} <command> [options]
|
|
844
|
+
|
|
845
|
+
${group.description}
|
|
846
|
+
|
|
847
|
+
Commands:`);
|
|
848
|
+
for (const [name, definition] of Object.entries(group.subcommands)) {
|
|
849
|
+
console.log(` ${name.padEnd(16)} ${definition.description}`);
|
|
850
|
+
}
|
|
851
|
+
console.log("\nRun `tt-local " + command + " <command> --help` for command-specific help.");
|
|
852
|
+
}
|
|
853
|
+
function parseCommandArguments(tokens, definition) {
|
|
854
|
+
const options = new Map(definition.options.map((option) => [option.name, option]));
|
|
855
|
+
const seen = /* @__PURE__ */ new Set();
|
|
856
|
+
const positionals = [];
|
|
857
|
+
let optionsEnded = false;
|
|
858
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
859
|
+
const token = tokens[index];
|
|
860
|
+
if (!optionsEnded && token === "--") {
|
|
861
|
+
optionsEnded = true;
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (!optionsEnded && token.startsWith("-")) {
|
|
865
|
+
const equalsIndex = token.indexOf("=");
|
|
866
|
+
const name = equalsIndex === -1 ? token : token.slice(0, equalsIndex);
|
|
867
|
+
const inlineValue = equalsIndex === -1 ? void 0 : token.slice(equalsIndex + 1);
|
|
868
|
+
const option = options.get(name);
|
|
869
|
+
if (!option) throw new Error(`Unknown option: ${name}`);
|
|
870
|
+
if (seen.has(name)) throw new Error(`Option ${name} may only be specified once.`);
|
|
871
|
+
seen.add(name);
|
|
872
|
+
if (option.value) {
|
|
873
|
+
if (inlineValue !== void 0) {
|
|
874
|
+
if (!inlineValue) throw new Error(`Option ${name} requires a value.`);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const value = tokens[index + 1];
|
|
878
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
879
|
+
throw new Error(`Option ${name} requires a value.`);
|
|
880
|
+
}
|
|
881
|
+
index += 1;
|
|
882
|
+
} else if (inlineValue !== void 0) {
|
|
883
|
+
throw new Error(`Option ${name} does not accept a value.`);
|
|
884
|
+
}
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
positionals.push(token);
|
|
888
|
+
}
|
|
889
|
+
if (positionals.length < (definition.minPositionals ?? 0)) {
|
|
890
|
+
throw new Error(definition.missingPositionalsMessage ?? `Missing required argument. Usage: ${definition.usage}`);
|
|
891
|
+
}
|
|
892
|
+
if (definition.maxPositionals !== void 0 && positionals.length > definition.maxPositionals) {
|
|
893
|
+
throw new Error(`Too many arguments. Usage: ${definition.usage}`);
|
|
894
|
+
}
|
|
895
|
+
return positionals;
|
|
896
|
+
}
|
|
897
|
+
function parseCli(argv) {
|
|
898
|
+
const command = argv[2] ?? "info";
|
|
899
|
+
if (command === "--help" || command === "-h") {
|
|
900
|
+
return { command: "info", positionals: [], help: "top" };
|
|
901
|
+
}
|
|
902
|
+
if (command === "--version" || command === "-V") {
|
|
903
|
+
if (argv.length > 3) throw new Error(`${command} does not accept arguments.`);
|
|
904
|
+
return { command, positionals: [], help: "command" };
|
|
905
|
+
}
|
|
906
|
+
if (command.startsWith("-")) throw new Error(`Unknown option: ${command}`);
|
|
907
|
+
const definition = COMMAND_DEFINITIONS[command];
|
|
908
|
+
if (definition) {
|
|
909
|
+
if (hasHelpFlag(argv.slice(3))) {
|
|
910
|
+
return { command, positionals: [], help: "command", definition };
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
command,
|
|
914
|
+
positionals: parseCommandArguments(argv.slice(3), definition),
|
|
915
|
+
help: "top",
|
|
916
|
+
definition
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
const group = COMMAND_GROUPS[command];
|
|
920
|
+
if (!group) throw new Error(`Unknown command: ${command}`);
|
|
921
|
+
if (argv[3] === "--help" || argv[3] === "-h") {
|
|
922
|
+
return { command, positionals: [], help: "group" };
|
|
923
|
+
}
|
|
924
|
+
const candidate = argv[3];
|
|
925
|
+
let subcommand;
|
|
926
|
+
let tokenStart;
|
|
927
|
+
if (candidate && !candidate.startsWith("-")) {
|
|
928
|
+
subcommand = candidate;
|
|
929
|
+
tokenStart = 4;
|
|
930
|
+
} else if (group.defaultSubcommand) {
|
|
931
|
+
subcommand = group.defaultSubcommand;
|
|
932
|
+
tokenStart = 3;
|
|
933
|
+
} else {
|
|
934
|
+
throw new Error(`${command} requires a subcommand. Run 'tt-local ${command} --help'.`);
|
|
935
|
+
}
|
|
936
|
+
const subcommandDefinition = group.subcommands[subcommand];
|
|
937
|
+
if (!subcommandDefinition) throw new Error(`Unknown ${command} command: ${subcommand}`);
|
|
938
|
+
if (hasHelpFlag(argv.slice(tokenStart))) {
|
|
939
|
+
return { command, subcommand, positionals: [], help: "command", definition: subcommandDefinition };
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
command,
|
|
943
|
+
subcommand,
|
|
944
|
+
positionals: parseCommandArguments(argv.slice(tokenStart), subcommandDefinition),
|
|
945
|
+
help: "top",
|
|
946
|
+
definition: subcommandDefinition
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function readNumberOption(argv, name) {
|
|
950
|
+
const value = readOption(argv, name);
|
|
951
|
+
return value ? Number(value) : void 0;
|
|
952
|
+
}
|
|
953
|
+
async function selectedConfigPath(argv, adjacentTo) {
|
|
954
|
+
const explicitPath = readOption(argv, "--config");
|
|
955
|
+
if (explicitPath) return resolve3(explicitPath);
|
|
956
|
+
const candidate = join3(adjacentTo ? dirname(resolve3(adjacentTo)) : cwd(), "local-runner.json");
|
|
957
|
+
const metadata = await stat(candidate).catch(() => null);
|
|
958
|
+
return metadata?.isFile() ? candidate : void 0;
|
|
959
|
+
}
|
|
960
|
+
async function configSelectionFromArgv(argv, adjacentTo) {
|
|
961
|
+
const path = await selectedConfigPath(argv, adjacentTo);
|
|
962
|
+
return {
|
|
963
|
+
config: await loadLocalRunnerConfig(path),
|
|
964
|
+
...path ? { path } : {}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
async function configFromArgv(argv, adjacentTo) {
|
|
968
|
+
return (await configSelectionFromArgv(argv, adjacentTo)).config;
|
|
969
|
+
}
|
|
970
|
+
async function loadCliBehaviorSpec(inputPath, runId) {
|
|
971
|
+
const input = await loadLocalRunInput(inputPath, {
|
|
972
|
+
...runId ? { runId } : {}
|
|
973
|
+
});
|
|
974
|
+
if (input.kind !== "spec") {
|
|
975
|
+
throw new Error(`TT Local CLI expects a tunedtensor.json behavior spec, not a full run request: ${input.path}`);
|
|
976
|
+
}
|
|
977
|
+
return input;
|
|
978
|
+
}
|
|
979
|
+
function printJson(value) {
|
|
980
|
+
console.log(JSON.stringify(value, null, 2));
|
|
981
|
+
}
|
|
982
|
+
function shortValue(value) {
|
|
983
|
+
if (value === void 0 || value === null) return null;
|
|
984
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
985
|
+
return String(value);
|
|
986
|
+
}
|
|
987
|
+
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
function formatEvent(event) {
|
|
991
|
+
const detailText = Object.entries(event.details ?? {}).filter(([key]) => key !== "metrics").map(([key, value]) => {
|
|
992
|
+
const formatted = key === "command" && Array.isArray(value) ? value.join(" ") : shortValue(value);
|
|
993
|
+
return formatted ? `${key}=${formatted}` : null;
|
|
994
|
+
}).filter((value) => Boolean(value)).slice(0, 5).join(" ");
|
|
995
|
+
return sanitizeLogLine(`[tt-local] ${event.stage}: ${event.message}${detailText ? ` (${detailText})` : ""}`);
|
|
996
|
+
}
|
|
997
|
+
function createConsoleReporter(options) {
|
|
998
|
+
if (options.quiet) return void 0;
|
|
999
|
+
let lastLogLine = "";
|
|
1000
|
+
return {
|
|
1001
|
+
verbose: options.verbose,
|
|
1002
|
+
onEvent(event) {
|
|
1003
|
+
process.stderr.write(`${formatEvent(event)}
|
|
1004
|
+
`);
|
|
1005
|
+
},
|
|
1006
|
+
onLog(log) {
|
|
1007
|
+
const line = sanitizeLogLine(`[tt-local] ${log.stage}${log.stream ? ` ${log.stream}` : ""}: ${log.message}`);
|
|
1008
|
+
if (line === lastLogLine) return;
|
|
1009
|
+
lastLogLine = line;
|
|
1010
|
+
process.stderr.write(`${line}
|
|
1011
|
+
`);
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
async function verifyStoredModel(model) {
|
|
1016
|
+
const manifestPath = join3(model.artifact_dir, "artifact-manifest.json");
|
|
1017
|
+
const integrity = await assertArtifactManifest(manifestPath, {
|
|
1018
|
+
requiredPaths: ["stage-metadata.json", "training-report.json"],
|
|
1019
|
+
scopeToRequired: true,
|
|
1020
|
+
verifyModel: true
|
|
1021
|
+
});
|
|
1022
|
+
const manifest = JSON.parse(await readFile2(manifestPath, "utf8"));
|
|
1023
|
+
if (!manifest.model) {
|
|
1024
|
+
throw new Error(`Artifact manifest does not contain a model contract: ${manifestPath}`);
|
|
1025
|
+
}
|
|
1026
|
+
const artifact = await assertUsableModelArtifact(model.artifact_uri);
|
|
1027
|
+
if (typeof manifest.model.base_model_artifact_uri === "string" && typeof manifest.model.base_model_fingerprint === "string") {
|
|
1028
|
+
const actualBaseFingerprint = await fingerprintLocalBaseModel(manifest.model.base_model_artifact_uri);
|
|
1029
|
+
if (actualBaseFingerprint !== manifest.model.base_model_fingerprint) {
|
|
1030
|
+
throw new Error("Recorded local base-model content no longer matches the model artifact contract.");
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (typeof manifest.model.artifact_root !== "string" || resolve3(manifest.model.artifact_root) !== resolve3(artifact.path)) {
|
|
1034
|
+
throw new Error("Stored model record does not match the artifact covered by its manifest.");
|
|
1035
|
+
}
|
|
1036
|
+
if (manifest.model.base_model !== model.base_model) {
|
|
1037
|
+
throw new Error("Stored model base model does not match its artifact manifest.");
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
manifest_path: manifestPath,
|
|
1041
|
+
integrity,
|
|
1042
|
+
artifact,
|
|
1043
|
+
contract: manifest.model
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
async function verifyActivationEvidence(model) {
|
|
1047
|
+
await assertArtifactManifest(join3(model.artifact_dir, "artifact-manifest.json"), {
|
|
1048
|
+
requiredPaths: [
|
|
1049
|
+
"run-report.json",
|
|
1050
|
+
"general-baseline-eval.json",
|
|
1051
|
+
"general-candidate-eval.json"
|
|
1052
|
+
],
|
|
1053
|
+
scopeToRequired: true,
|
|
1054
|
+
verifyModel: true
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
async function verifyModelArtifactPath(input) {
|
|
1058
|
+
const inputPath = resolve3(input);
|
|
1059
|
+
let manifestPath;
|
|
1060
|
+
let artifactUri = inputPath;
|
|
1061
|
+
if (basename(inputPath) === "artifact-manifest.json") {
|
|
1062
|
+
manifestPath = inputPath;
|
|
1063
|
+
const raw = JSON.parse(await readFile2(inputPath, "utf8"));
|
|
1064
|
+
if (typeof raw.model?.artifact_root !== "string") {
|
|
1065
|
+
throw new Error(`Artifact manifest does not contain a model contract: ${inputPath}`);
|
|
1066
|
+
}
|
|
1067
|
+
artifactUri = raw.model.artifact_root;
|
|
1068
|
+
} else {
|
|
1069
|
+
const metadata = await stat(inputPath).catch(() => null);
|
|
1070
|
+
if (!metadata) throw new Error(`Model not found and artifact path does not exist: ${input}`);
|
|
1071
|
+
let current = metadata.isDirectory() ? inputPath : dirname(inputPath);
|
|
1072
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
1073
|
+
const candidate = join3(current, "artifact-manifest.json");
|
|
1074
|
+
const raw = await readFile2(candidate, "utf8").catch(() => null);
|
|
1075
|
+
if (raw) {
|
|
1076
|
+
const parsed = JSON.parse(raw);
|
|
1077
|
+
if (typeof parsed.model?.artifact_root === "string" && resolve3(parsed.model.artifact_root) === resolve3(inputPath)) {
|
|
1078
|
+
manifestPath = candidate;
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const parent = dirname(current);
|
|
1083
|
+
if (parent === current) break;
|
|
1084
|
+
current = parent;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (!manifestPath) {
|
|
1088
|
+
throw new Error(`No artifact manifest covering model path ${inputPath} was found in its parent run directory.`);
|
|
1089
|
+
}
|
|
1090
|
+
const integrity = await assertArtifactManifest(manifestPath, {
|
|
1091
|
+
requiredPaths: ["stage-metadata.json", "training-report.json"],
|
|
1092
|
+
scopeToRequired: true,
|
|
1093
|
+
verifyModel: true
|
|
1094
|
+
});
|
|
1095
|
+
const manifest = JSON.parse(await readFile2(manifestPath, "utf8"));
|
|
1096
|
+
if (!manifest.model) throw new Error(`Artifact manifest does not contain a model contract: ${manifestPath}`);
|
|
1097
|
+
const artifact = await assertUsableModelArtifact(artifactUri);
|
|
1098
|
+
if (typeof manifest.model.base_model_artifact_uri === "string" && typeof manifest.model.base_model_fingerprint === "string") {
|
|
1099
|
+
const actualBaseFingerprint = await fingerprintLocalBaseModel(manifest.model.base_model_artifact_uri);
|
|
1100
|
+
if (actualBaseFingerprint !== manifest.model.base_model_fingerprint) {
|
|
1101
|
+
throw new Error("Recorded local base-model content no longer matches the model artifact contract.");
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (typeof manifest.model.artifact_root !== "string" || resolve3(manifest.model.artifact_root) !== resolve3(artifact.path)) {
|
|
1105
|
+
throw new Error("Model path does not match the artifact covered by its manifest.");
|
|
1106
|
+
}
|
|
1107
|
+
return { manifest_path: manifestPath, integrity, artifact, contract: manifest.model };
|
|
1108
|
+
}
|
|
1109
|
+
async function modelSystemPrompt(args) {
|
|
1110
|
+
const specPath = readOption(args.argv, "--spec");
|
|
1111
|
+
if (specPath && hasFlag(args.argv, "--no-spec-prompt")) {
|
|
1112
|
+
throw new Error("Use only one of --spec or --no-spec-prompt.");
|
|
1113
|
+
}
|
|
1114
|
+
if (hasFlag(args.argv, "--no-spec-prompt")) return void 0;
|
|
1115
|
+
let spec;
|
|
1116
|
+
if (specPath) {
|
|
1117
|
+
const input = JSON.parse(await readFile2(resolve3(specPath), "utf8"));
|
|
1118
|
+
const local = localBehaviorSpecFileSchema.safeParse(input);
|
|
1119
|
+
if (!local.success) {
|
|
1120
|
+
throw new Error(`--spec must contain a tunedtensor.json behavior spec: ${resolve3(specPath)}`);
|
|
1121
|
+
}
|
|
1122
|
+
spec = local.data;
|
|
1123
|
+
} else {
|
|
1124
|
+
const runRequestPath = join3(args.store.paths.runsDir, args.model.run_id, "request.json");
|
|
1125
|
+
let persistedRequest = null;
|
|
1126
|
+
try {
|
|
1127
|
+
persistedRequest = fineTuneRunRequestSchema.parse(JSON.parse(await readFile2(runRequestPath, "utf8")));
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (error.code !== "ENOENT") throw error;
|
|
1130
|
+
}
|
|
1131
|
+
spec = persistedRequest?.spec_snapshot ?? (await args.store.getSpec(args.model.behavior_spec_id)).spec;
|
|
1132
|
+
}
|
|
1133
|
+
if (spec.base_model !== args.model.base_model) {
|
|
1134
|
+
throw new Error(
|
|
1135
|
+
`Behavior spec base model ${spec.base_model} does not match stored model base ${args.model.base_model}.`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
const prompt = buildSystemMessage(spec);
|
|
1139
|
+
const metadata = JSON.parse(
|
|
1140
|
+
await readFile2(join3(args.model.artifact_dir, "stage-metadata.json"), "utf8")
|
|
1141
|
+
);
|
|
1142
|
+
const promptHash = createHash("sha256").update(prompt).digest("hex");
|
|
1143
|
+
if (metadata.system_prompt_sha256 !== promptHash) {
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
"Behavior spec instructions do not match the prompt fingerprint used for this trained model. Pass the original --spec, or use --no-spec-prompt only when this change is intentional."
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
return prompt;
|
|
1149
|
+
}
|
|
1150
|
+
function modelServeDevice(argv) {
|
|
1151
|
+
const value = readOption(argv, "--device");
|
|
1152
|
+
if (value === void 0 || value === "cpu" || value === "cuda") {
|
|
1153
|
+
return value;
|
|
1154
|
+
}
|
|
1155
|
+
throw new Error(`--device must be cuda or cpu; got: ${value}`);
|
|
1156
|
+
}
|
|
1157
|
+
async function serveStoredModelFromCli(args) {
|
|
1158
|
+
const store = createLocalStore(args.config.storeRoot);
|
|
1159
|
+
const model = await store.getModel(args.modelId);
|
|
1160
|
+
const verified = await verifyStoredModel(model);
|
|
1161
|
+
const launch = buildLocalModelServerLaunch({
|
|
1162
|
+
model,
|
|
1163
|
+
config: args.config,
|
|
1164
|
+
options: {
|
|
1165
|
+
host: readOption(args.argv, "--host"),
|
|
1166
|
+
port: readNumberOption(args.argv, "--port"),
|
|
1167
|
+
device: modelServeDevice(args.argv),
|
|
1168
|
+
maxTokens: readNumberOption(args.argv, "--max-tokens"),
|
|
1169
|
+
temperature: readNumberOption(args.argv, "--temperature"),
|
|
1170
|
+
topP: readNumberOption(args.argv, "--top-p"),
|
|
1171
|
+
maxConcurrentRequests: readNumberOption(args.argv, "--max-concurrent-requests"),
|
|
1172
|
+
systemPrompt: await modelSystemPrompt({ argv: args.argv, model, store }),
|
|
1173
|
+
allowRemote: hasFlag(args.argv, "--allow-remote"),
|
|
1174
|
+
apiKeyEnv: readOption(args.argv, "--api-key-env"),
|
|
1175
|
+
baseModelRevision: (() => {
|
|
1176
|
+
const contract = verified.contract;
|
|
1177
|
+
return typeof contract.base_model_revision === "string" ? contract.base_model_revision : void 0;
|
|
1178
|
+
})(),
|
|
1179
|
+
baseModelArtifactUri: (() => {
|
|
1180
|
+
const contract = verified.contract;
|
|
1181
|
+
return typeof contract.base_model_artifact_uri === "string" ? contract.base_model_artifact_uri : void 0;
|
|
1182
|
+
})()
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
if (hasFlag(args.argv, "--print-command")) {
|
|
1186
|
+
printJson({
|
|
1187
|
+
ok: true,
|
|
1188
|
+
model_id: model.id,
|
|
1189
|
+
url: launch.url,
|
|
1190
|
+
command: launch.displayCommand,
|
|
1191
|
+
artifact_path: launch.artifactPath,
|
|
1192
|
+
manifest_path: verified.manifest_path,
|
|
1193
|
+
integrity: verified.integrity
|
|
1194
|
+
});
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
process.stderr.write(`[tt-local] verified ${verified.integrity.checked} artifact file(s)
|
|
1198
|
+
`);
|
|
1199
|
+
process.stderr.write(`[tt-local] model API: ${launch.url}
|
|
1200
|
+
`);
|
|
1201
|
+
await serveLocalModel(launch);
|
|
1202
|
+
}
|
|
1203
|
+
function serveOptionsFromArgv(argv) {
|
|
1204
|
+
return {
|
|
1205
|
+
host: readOption(argv, "--host"),
|
|
1206
|
+
port: readNumberOption(argv, "--port"),
|
|
1207
|
+
device: modelServeDevice(argv),
|
|
1208
|
+
maxTokens: readNumberOption(argv, "--max-tokens"),
|
|
1209
|
+
temperature: readNumberOption(argv, "--temperature"),
|
|
1210
|
+
topP: readNumberOption(argv, "--top-p"),
|
|
1211
|
+
maxConcurrentRequests: readNumberOption(argv, "--max-concurrent-requests"),
|
|
1212
|
+
allowRemote: hasFlag(argv, "--allow-remote"),
|
|
1213
|
+
apiKeyEnv: readOption(argv, "--api-key-env")
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
async function serveBaseModelFromCli(args) {
|
|
1217
|
+
const explicitSpecPath = readOption(args.argv, "--spec");
|
|
1218
|
+
if (explicitSpecPath && hasFlag(args.argv, "--no-spec-prompt")) {
|
|
1219
|
+
throw new Error("Use only one of --spec or --no-spec-prompt.");
|
|
1220
|
+
}
|
|
1221
|
+
const defaultSpecPath = resolve3(DEFAULT_LOCAL_SPEC_PATH);
|
|
1222
|
+
const specPath = explicitSpecPath ? resolve3(explicitSpecPath) : (await stat(defaultSpecPath).catch(() => null))?.isFile() ? defaultSpecPath : void 0;
|
|
1223
|
+
const spec = specPath ? localBehaviorSpecFileSchema.parse(
|
|
1224
|
+
JSON.parse(await readFile2(specPath, "utf8"))
|
|
1225
|
+
) : void 0;
|
|
1226
|
+
let baseModel = spec?.base_model;
|
|
1227
|
+
if (!baseModel) {
|
|
1228
|
+
const store = createLocalStore(args.config.storeRoot);
|
|
1229
|
+
const active = await getActiveModel(store);
|
|
1230
|
+
const referenceModel = active.model ?? (active.pointer?.previous_model_id ? await store.getModel(active.pointer.previous_model_id) : null);
|
|
1231
|
+
baseModel = referenceModel?.base_model;
|
|
1232
|
+
}
|
|
1233
|
+
if (!baseModel) {
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
"Cannot identify the protected base model. Pass --spec <tunedtensor.json> or activate a model first."
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
const launch = buildLocalBaseModelServerLaunch({
|
|
1239
|
+
baseModel,
|
|
1240
|
+
config: args.config,
|
|
1241
|
+
options: {
|
|
1242
|
+
...serveOptionsFromArgv(args.argv),
|
|
1243
|
+
systemPrompt: explicitSpecPath && !hasFlag(args.argv, "--no-spec-prompt") ? buildSystemMessage(spec) : void 0,
|
|
1244
|
+
baseModelRevision: spec?.hyperparameters?.base_model_revision
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
if (hasFlag(args.argv, "--print-command")) {
|
|
1248
|
+
printJson({
|
|
1249
|
+
ok: true,
|
|
1250
|
+
model_id: null,
|
|
1251
|
+
base_model: baseModel,
|
|
1252
|
+
url: launch.url,
|
|
1253
|
+
command: launch.displayCommand
|
|
1254
|
+
});
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
process.stderr.write(`[tt-local] protected base model API: ${launch.url}
|
|
1258
|
+
`);
|
|
1259
|
+
await serveLocalModel(launch);
|
|
1260
|
+
}
|
|
1261
|
+
async function serveModelTargetFromCli(args) {
|
|
1262
|
+
if (args.target === "base") {
|
|
1263
|
+
return serveBaseModelFromCli(args);
|
|
1264
|
+
}
|
|
1265
|
+
if (args.target === "active") {
|
|
1266
|
+
const store = createLocalStore(args.config.storeRoot);
|
|
1267
|
+
const active = await getActiveModel(store);
|
|
1268
|
+
if (!active.model) return serveBaseModelFromCli(args);
|
|
1269
|
+
return serveStoredModelFromCli({
|
|
1270
|
+
argv: args.argv,
|
|
1271
|
+
modelId: active.model.id,
|
|
1272
|
+
config: args.config
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
return serveStoredModelFromCli({
|
|
1276
|
+
argv: args.argv,
|
|
1277
|
+
modelId: args.target,
|
|
1278
|
+
config: args.config
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
async function main(argv) {
|
|
1282
|
+
const cli = parseCli(argv);
|
|
1283
|
+
const command = cli.command;
|
|
1284
|
+
if (command === "--version" || command === "-V") {
|
|
1285
|
+
console.log(TT_LOCAL_VERSION);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (cli.help === "top" && (argv[2] === "--help" || argv[2] === "-h")) {
|
|
1289
|
+
printHelp();
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (cli.help === "group") {
|
|
1293
|
+
printGroupHelp(command, COMMAND_GROUPS[command]);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (cli.help === "command" && cli.definition) {
|
|
1297
|
+
printCommandHelp(cli.definition);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (command === "info") {
|
|
1301
|
+
const info = getLocalRunnerInfo();
|
|
1302
|
+
console.log(`${info.name}: ${info.description}`);
|
|
1303
|
+
console.log(`Version: ${info.version}`);
|
|
1304
|
+
console.log(`Status: ${info.status}`);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (command === "doctor") {
|
|
1308
|
+
const inputPath = resolve3(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
1309
|
+
const configSelection = await configSelectionFromArgv(argv, inputPath);
|
|
1310
|
+
const hasDefaultSpec = cli.positionals[0] ? true : Boolean((await stat(inputPath).catch(() => null))?.isFile());
|
|
1311
|
+
const request = hasDefaultSpec ? (await loadCliBehaviorSpec(inputPath)).request : void 0;
|
|
1312
|
+
const checks = await runDoctor(configSelection.config, request);
|
|
1313
|
+
const ok = checks.every((check) => check.ok);
|
|
1314
|
+
printJson({
|
|
1315
|
+
ok,
|
|
1316
|
+
config_path: configSelection.path ?? null,
|
|
1317
|
+
checks
|
|
1318
|
+
});
|
|
1319
|
+
if (!ok) process.exitCode = 1;
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (command === "init") {
|
|
1323
|
+
const outputPath = resolve3(readOption(argv, "--output") ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
1324
|
+
const profile = readOption(argv, "--profile");
|
|
1325
|
+
if (profile !== void 0 && profile !== "spark") {
|
|
1326
|
+
throw new Error(`--profile must be spark, got: ${profile}`);
|
|
1327
|
+
}
|
|
1328
|
+
const spec = await initLocalSpecFile({
|
|
1329
|
+
outputPath,
|
|
1330
|
+
name: readOption(argv, "--name") ?? "Local Tuned Tensor Spec",
|
|
1331
|
+
baseModel: readOption(argv, "--model") ?? "Qwen/Qwen3.5-2B",
|
|
1332
|
+
force: hasFlag(argv, "--force")
|
|
1333
|
+
});
|
|
1334
|
+
const configPath = profile ? resolve3(readOption(argv, "--config") ?? resolve3(dirname(outputPath), "local-runner.json")) : await selectedConfigPath(argv, outputPath);
|
|
1335
|
+
if (profile) {
|
|
1336
|
+
await initLocalRunnerConfigFile({
|
|
1337
|
+
outputPath: configPath,
|
|
1338
|
+
profile,
|
|
1339
|
+
force: hasFlag(argv, "--force")
|
|
1340
|
+
});
|
|
1341
|
+
} else if (configPath) {
|
|
1342
|
+
await loadLocalRunnerConfig(configPath);
|
|
1343
|
+
}
|
|
1344
|
+
printJson({
|
|
1345
|
+
ok: true,
|
|
1346
|
+
path: outputPath,
|
|
1347
|
+
id: spec.id,
|
|
1348
|
+
name: spec.name,
|
|
1349
|
+
base_model: spec.base_model,
|
|
1350
|
+
config_path: configPath ?? null
|
|
1351
|
+
});
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (command === "validate") {
|
|
1355
|
+
const inputPath = resolve3(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
1356
|
+
const input = await loadCliBehaviorSpec(inputPath);
|
|
1357
|
+
assertLocalRunInputReady(input.request);
|
|
1358
|
+
const configSelection = await configSelectionFromArgv(argv, inputPath);
|
|
1359
|
+
const validated = await validateLocalFineTuneInput({
|
|
1360
|
+
request: input.request,
|
|
1361
|
+
config: configSelection.config
|
|
1362
|
+
});
|
|
1363
|
+
const request = validated.request;
|
|
1364
|
+
const config = validated.config;
|
|
1365
|
+
printJson({
|
|
1366
|
+
ok: true,
|
|
1367
|
+
input_path: input.path,
|
|
1368
|
+
config_path: configSelection.path ?? null,
|
|
1369
|
+
behavior_spec_id: request.behavior_spec_id,
|
|
1370
|
+
base_model: request.spec_snapshot.base_model,
|
|
1371
|
+
dataset_format: request.dataset_prebuilt?.format ?? null,
|
|
1372
|
+
artifact_root: config.artifactRoot,
|
|
1373
|
+
store_root: config.storeRoot,
|
|
1374
|
+
dry_run: config.dryRun
|
|
1375
|
+
});
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (command === "run") {
|
|
1379
|
+
const inputPath = resolve3(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
1380
|
+
const configSelection = await configSelectionFromArgv(argv, inputPath);
|
|
1381
|
+
const configInput = configSelection.config;
|
|
1382
|
+
const config = localRunnerConfigSchema.parse({
|
|
1383
|
+
...configInput,
|
|
1384
|
+
dryRun: hasFlag(argv, "--dry-run") ? true : configInput.dryRun
|
|
1385
|
+
});
|
|
1386
|
+
const input = await loadCliBehaviorSpec(inputPath);
|
|
1387
|
+
assertLocalRunInputReady(input.request);
|
|
1388
|
+
const validated = await validateLocalFineTuneInput({
|
|
1389
|
+
request: input.request,
|
|
1390
|
+
config
|
|
1391
|
+
});
|
|
1392
|
+
let request = validated.request;
|
|
1393
|
+
const reporter = createConsoleReporter({
|
|
1394
|
+
verbose: hasFlag(argv, "--verbose"),
|
|
1395
|
+
quiet: hasFlag(argv, "--quiet")
|
|
1396
|
+
});
|
|
1397
|
+
if (!config.dryRun && !config.paths.baseModel && !request.hyperparameters.base_model_revision) {
|
|
1398
|
+
const prefetch = await prefetchBaseModel({
|
|
1399
|
+
request,
|
|
1400
|
+
config,
|
|
1401
|
+
reporter
|
|
1402
|
+
});
|
|
1403
|
+
if (!prefetch.snapshot_revision) {
|
|
1404
|
+
throw new Error("Base-model prefetch did not return an immutable snapshot revision.");
|
|
1405
|
+
}
|
|
1406
|
+
request = fineTuneRunRequestSchema.parse({
|
|
1407
|
+
...request,
|
|
1408
|
+
hyperparameters: {
|
|
1409
|
+
...request.hyperparameters,
|
|
1410
|
+
base_model_revision: prefetch.snapshot_revision
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
const result = await runLocalFineTune({
|
|
1415
|
+
request,
|
|
1416
|
+
config,
|
|
1417
|
+
reporter
|
|
1418
|
+
});
|
|
1419
|
+
printJson({
|
|
1420
|
+
status: result.report.status,
|
|
1421
|
+
run_id: result.report.run_id,
|
|
1422
|
+
behavior_spec_id: result.report.behavior_spec_id,
|
|
1423
|
+
report_path: result.reportPath,
|
|
1424
|
+
artifact_dir: result.artifactDir,
|
|
1425
|
+
...!config.dryRun ? {
|
|
1426
|
+
model_id: `local-${result.report.run_id}`,
|
|
1427
|
+
fine_tuned_model_id: result.report.fine_tuned_model_id
|
|
1428
|
+
} : {},
|
|
1429
|
+
training_log: result.report.training.log_uri,
|
|
1430
|
+
baseline_eval: result.report.artifact_uris.baseline_eval,
|
|
1431
|
+
candidate_eval: result.report.artifact_uris.candidate_eval,
|
|
1432
|
+
comparison: result.report.comparison,
|
|
1433
|
+
general_regression: result.report.general_regression
|
|
1434
|
+
});
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (command === "serve") {
|
|
1438
|
+
const config = await configFromArgv(argv);
|
|
1439
|
+
await serveModelTargetFromCli({
|
|
1440
|
+
argv,
|
|
1441
|
+
target: cli.positionals[0],
|
|
1442
|
+
config
|
|
1443
|
+
});
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
if (command === "runs") {
|
|
1447
|
+
const subcommand = cli.subcommand;
|
|
1448
|
+
const config = await configFromArgv(argv);
|
|
1449
|
+
const store = createLocalStore(config.storeRoot);
|
|
1450
|
+
if (subcommand === "list") return printJson(await store.listRuns());
|
|
1451
|
+
if (subcommand === "get") {
|
|
1452
|
+
const id = cli.positionals[0];
|
|
1453
|
+
if (!id) throw new Error("runs get requires <run-id>");
|
|
1454
|
+
return printJson(await store.getRun(id));
|
|
1455
|
+
}
|
|
1456
|
+
if (subcommand === "events") {
|
|
1457
|
+
const id = cli.positionals[0];
|
|
1458
|
+
if (!id) throw new Error("runs events requires <run-id>");
|
|
1459
|
+
return printJson(await store.getRunEvents(id));
|
|
1460
|
+
}
|
|
1461
|
+
if (subcommand === "report") {
|
|
1462
|
+
const id = cli.positionals[0];
|
|
1463
|
+
if (!id) throw new Error("runs report requires <run-id>");
|
|
1464
|
+
return printJson(await store.getRunReport(id));
|
|
1465
|
+
}
|
|
1466
|
+
if (subcommand === "compare") {
|
|
1467
|
+
const idA = cli.positionals[0];
|
|
1468
|
+
const idB = cli.positionals[1];
|
|
1469
|
+
if (!idA || !idB) throw new Error("runs compare requires <run-id-a> <run-id-b>");
|
|
1470
|
+
const [reportA, reportB] = await Promise.all([
|
|
1471
|
+
store.getRunReport(idA),
|
|
1472
|
+
store.getRunReport(idB)
|
|
1473
|
+
]);
|
|
1474
|
+
return printJson(compareRuns(reportA, reportB));
|
|
1475
|
+
}
|
|
1476
|
+
throw new Error(`Unknown runs command: ${subcommand}`);
|
|
1477
|
+
}
|
|
1478
|
+
if (command === "models") {
|
|
1479
|
+
const subcommand = cli.subcommand;
|
|
1480
|
+
const modelInputPath = subcommand === "prefetch" || subcommand === "verify-base" ? resolve3(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH) : void 0;
|
|
1481
|
+
const config = await configFromArgv(argv, modelInputPath);
|
|
1482
|
+
const store = createLocalStore(config.storeRoot);
|
|
1483
|
+
if (subcommand === "list") return printJson(await store.listModels());
|
|
1484
|
+
if (subcommand === "get") {
|
|
1485
|
+
const id = cli.positionals[0];
|
|
1486
|
+
if (!id) throw new Error("models get requires <model-id>");
|
|
1487
|
+
return printJson(await store.getModel(id));
|
|
1488
|
+
}
|
|
1489
|
+
if (subcommand === "verify") {
|
|
1490
|
+
const id = cli.positionals[0];
|
|
1491
|
+
if (!id) throw new Error("models verify requires <model-id-or-artifact-path>");
|
|
1492
|
+
if (await stat(resolve3(id)).then(() => true, () => false)) {
|
|
1493
|
+
return printJson({ ok: true, model: null, ...await verifyModelArtifactPath(id) });
|
|
1494
|
+
}
|
|
1495
|
+
const model = await store.getModel(id);
|
|
1496
|
+
const verified = await verifyStoredModel(model);
|
|
1497
|
+
return printJson({ ok: true, model, ...verified });
|
|
1498
|
+
}
|
|
1499
|
+
if (subcommand === "active") {
|
|
1500
|
+
const active = await getActiveModel(store);
|
|
1501
|
+
return printJson({
|
|
1502
|
+
active: active.model?.id ?? "base",
|
|
1503
|
+
pointer: active.pointer,
|
|
1504
|
+
model: active.model
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
if (subcommand === "activate") {
|
|
1508
|
+
const id = cli.positionals[0];
|
|
1509
|
+
if (!id) throw new Error("models activate requires <model-id>");
|
|
1510
|
+
const model = await store.getModel(id);
|
|
1511
|
+
const verified = await verifyStoredModel(model);
|
|
1512
|
+
await verifyActivationEvidence(model);
|
|
1513
|
+
const pointer = await activateModel(store, model.id);
|
|
1514
|
+
return printJson({
|
|
1515
|
+
active: pointer.model_id,
|
|
1516
|
+
pointer,
|
|
1517
|
+
manifest_path: verified.manifest_path,
|
|
1518
|
+
integrity: verified.integrity
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
if (subcommand === "rollback") {
|
|
1522
|
+
const current = await getActiveModel(store);
|
|
1523
|
+
if (current.pointer?.previous_model_id) {
|
|
1524
|
+
const previous = await store.getModel(current.pointer.previous_model_id);
|
|
1525
|
+
await verifyStoredModel(previous);
|
|
1526
|
+
await verifyActivationEvidence(previous);
|
|
1527
|
+
}
|
|
1528
|
+
const pointer = await rollbackActiveModel(store);
|
|
1529
|
+
return printJson({ active: pointer.model_id ?? "base", pointer });
|
|
1530
|
+
}
|
|
1531
|
+
if (subcommand === "prefetch" || subcommand === "verify-base") {
|
|
1532
|
+
const input = await loadCliBehaviorSpec(modelInputPath);
|
|
1533
|
+
const report = await prefetchBaseModel({
|
|
1534
|
+
request: input.request,
|
|
1535
|
+
config,
|
|
1536
|
+
localOnly: subcommand === "verify-base",
|
|
1537
|
+
reporter: createConsoleReporter({
|
|
1538
|
+
verbose: hasFlag(argv, "--verbose"),
|
|
1539
|
+
quiet: hasFlag(argv, "--quiet")
|
|
1540
|
+
})
|
|
1541
|
+
});
|
|
1542
|
+
return printJson({
|
|
1543
|
+
...report,
|
|
1544
|
+
input_path: input.path
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
if (subcommand === "serve") {
|
|
1548
|
+
const id = cli.positionals[0];
|
|
1549
|
+
if (!id) throw new Error("models serve requires <model-id>");
|
|
1550
|
+
await serveModelTargetFromCli({ argv, target: id, config });
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
throw new Error(`Unknown models command: ${subcommand}`);
|
|
1554
|
+
}
|
|
1555
|
+
console.error(`Unknown command: ${command}`);
|
|
1556
|
+
process.exitCode = 1;
|
|
1557
|
+
}
|
|
1558
|
+
function isCliEntrypoint() {
|
|
1559
|
+
if (!process.argv[1]) return false;
|
|
1560
|
+
return realpathSync(fileURLToPath2(import.meta.url)) === realpathSync(process.argv[1]);
|
|
1561
|
+
}
|
|
1562
|
+
if (isCliEntrypoint()) {
|
|
1563
|
+
main(process.argv).catch((error) => {
|
|
1564
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1565
|
+
process.exitCode = 1;
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
export {
|
|
1569
|
+
DEFAULT_LOCAL_SPEC_PATH,
|
|
1570
|
+
TT_LOCAL_VERSION,
|
|
1571
|
+
activateModel,
|
|
1572
|
+
assertLocalRunInputReady,
|
|
1573
|
+
baseModelRevisionSchema,
|
|
1574
|
+
behaviorSpecExampleSchema,
|
|
1575
|
+
buildLocalBaseModelServerLaunch,
|
|
1576
|
+
buildLocalModelServerLaunch,
|
|
1577
|
+
buildModelPrefetchPayload,
|
|
1578
|
+
buildSystemMessage,
|
|
1579
|
+
canonicalLocalPipeline,
|
|
1580
|
+
compareRuns,
|
|
1581
|
+
comparisonReportSchema,
|
|
1582
|
+
compileSpecToJsonl,
|
|
1583
|
+
createLocalStore,
|
|
1584
|
+
datasetFormatSchema,
|
|
1585
|
+
datasetPrebuiltSchema,
|
|
1586
|
+
defaultLocalHome,
|
|
1587
|
+
evalExampleResultSchema,
|
|
1588
|
+
evalReportSchema,
|
|
1589
|
+
evalSplitSchema,
|
|
1590
|
+
evaluateGeneralRegressionGate,
|
|
1591
|
+
evaluationSuiteFromChatJsonl,
|
|
1592
|
+
exampleToChatRow,
|
|
1593
|
+
examplesFromChatJsonl,
|
|
1594
|
+
examplesFromSpec,
|
|
1595
|
+
fineTuneHyperparametersSchema,
|
|
1596
|
+
fineTuneRunRequestSchema,
|
|
1597
|
+
fingerprintLocalBaseModel,
|
|
1598
|
+
forwardStreamLines,
|
|
1599
|
+
generatedPlaceholderIssues,
|
|
1600
|
+
getActiveModel,
|
|
1601
|
+
getLocalRunnerInfo,
|
|
1602
|
+
initLocalRunnerConfigFile,
|
|
1603
|
+
initLocalSpecFile,
|
|
1604
|
+
isTerminalRunState,
|
|
1605
|
+
jsonFieldMetricsSchema,
|
|
1606
|
+
loadJsonFile,
|
|
1607
|
+
loadLocalRunInput,
|
|
1608
|
+
loadLocalRunnerConfig,
|
|
1609
|
+
loadRunRequest,
|
|
1610
|
+
localBehaviorSpecFileSchema,
|
|
1611
|
+
localRunnerConfigSchema,
|
|
1612
|
+
localStorePaths,
|
|
1613
|
+
normalizeChatJsonlForRelocation,
|
|
1614
|
+
prefetchBaseModel,
|
|
1615
|
+
reportInBackground,
|
|
1616
|
+
resolveLocalRunInputPaths,
|
|
1617
|
+
rollbackActiveModel,
|
|
1618
|
+
runLocalFineTune,
|
|
1619
|
+
runLocalPipeline,
|
|
1620
|
+
runReportSchema,
|
|
1621
|
+
runRequestFromLocalSpec,
|
|
1622
|
+
sanitizeLogLine,
|
|
1623
|
+
serveLocalModel,
|
|
1624
|
+
specSnapshotSchema,
|
|
1625
|
+
trainingReportSchema,
|
|
1626
|
+
validateLocalFineTuneInput,
|
|
1627
|
+
validateLocalPipeline,
|
|
1628
|
+
verifyLocalBaseModel
|
|
1629
|
+
};
|
|
1630
|
+
//# sourceMappingURL=local-runtime.js.map
|