@tuned-tensor/local 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +149 -0
- package/dist/artifacts.d.ts +92 -0
- package/dist/artifacts.js +591 -3
- package/dist/artifacts.js.map +1 -1
- package/dist/contracts.d.ts +5 -2
- package/dist/contracts.js +15 -10
- package/dist/contracts.js.map +1 -1
- package/dist/dataset.d.ts +3 -0
- package/dist/dataset.js +87 -5
- package/dist/dataset.js.map +1 -1
- package/dist/doctor.d.ts +13 -2
- package/dist/doctor.js +372 -49
- package/dist/doctor.js.map +1 -1
- package/dist/evaluation.d.ts +15 -0
- package/dist/evaluation.js +120 -16
- package/dist/evaluation.js.map +1 -1
- package/dist/huggingface-cache.d.ts +26 -0
- package/dist/huggingface-cache.js +68 -0
- package/dist/huggingface-cache.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +766 -73
- package/dist/index.js.map +1 -1
- package/dist/local-project.d.ts +11 -0
- package/dist/local-project.js +96 -3
- package/dist/local-project.js.map +1 -1
- package/dist/model-registry.d.ts +21 -0
- package/dist/model-registry.js +198 -0
- package/dist/model-registry.js.map +1 -1
- package/dist/model-server.d.ts +32 -0
- package/dist/model-server.js +158 -0
- package/dist/model-server.js.map +1 -0
- package/dist/orchestrator.d.ts +3 -0
- package/dist/orchestrator.js +1001 -142
- package/dist/orchestrator.js.map +1 -1
- package/dist/prefetch.d.ts +43 -0
- package/dist/prefetch.js +192 -0
- package/dist/prefetch.js.map +1 -0
- package/dist/process-runner.d.ts +7 -0
- package/dist/process-runner.js +171 -16
- package/dist/process-runner.js.map +1 -1
- package/dist/process-training.d.ts +5 -1
- package/dist/process-training.js +32 -16
- package/dist/process-training.js.map +1 -1
- package/dist/run-reporter.d.ts +2 -0
- package/dist/run-reporter.js +10 -0
- package/dist/run-reporter.js.map +1 -1
- package/dist/store.d.ts +16 -2
- package/dist/store.js +189 -24
- package/dist/store.js.map +1 -1
- package/docs/architecture.md +32 -0
- package/docs/local-workflow-remediation-2026-07-13.md +130 -0
- package/docs/local-workflow-ux-review-2026-07-13.md +458 -0
- package/docs/spark.md +10 -0
- package/package.json +3 -1
- package/training/local-runner/pyproject.toml +1 -0
- package/training/local-runner/src/evaluate.py +80 -20
- package/training/local-runner/src/prefetch.py +178 -0
- package/training/local-runner/src/serve.py +329 -0
- package/training/local-runner/src/train.py +47 -22
- package/training/local-runner/src/train_dpo.py +41 -25
- package/training/local-runner/uv.lock +2401 -0
package/dist/index.js
CHANGED
|
@@ -1,36 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdir, open, readFile, stat } from "node:fs/promises";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
7
|
import { cwd } from "node:process";
|
|
6
8
|
import { fileURLToPath } from "node:url";
|
|
7
9
|
import { compareRuns } from "./compare.js";
|
|
10
|
+
import { assertArtifactManifest, claimRunArtifactDirectory, defaultArtifactPrefix, resolveRunArtifacts } from "./artifacts.js";
|
|
8
11
|
import { fineTuneRunRequestSchema, localBehaviorSpecFileSchema, localRunnerConfigSchema, specSnapshotSchema } from "./contracts.js";
|
|
9
12
|
import { buildSystemMessage } from "./dataset.js";
|
|
13
|
+
import { assertEvaluationScoringReady } from "./evaluation.js";
|
|
10
14
|
import { runLocalLabelingJob } from "./labeling.js";
|
|
11
|
-
import { loadLocalRunnerConfig, runLocalFineTuneStage, } from "./orchestrator.js";
|
|
15
|
+
import { loadLocalRunnerConfig, fingerprintLocalBaseModel, runLocalFineTuneStage, } from "./orchestrator.js";
|
|
12
16
|
import { runDoctor } from "./doctor.js";
|
|
13
|
-
import { resolveTrainingModel } from "./model-registry.js";
|
|
14
|
-
import {
|
|
17
|
+
import { assertUsableModelArtifact, resolveTrainingModel } from "./model-registry.js";
|
|
18
|
+
import { buildLocalModelServerLaunch, serveLocalModel } from "./model-server.js";
|
|
19
|
+
import { prefetchBaseModel } from "./prefetch.js";
|
|
20
|
+
import { createLocalStore, isTerminalRunState } from "./store.js";
|
|
15
21
|
import { serveLocalDashboard } from "./server.js";
|
|
16
|
-
import { DEFAULT_LOCAL_SPEC_PATH, initLocalSpecFile, loadLocalRunInput, runRequestFromLocalSpec, } from "./local-project.js";
|
|
22
|
+
import { DEFAULT_LOCAL_SPEC_PATH, assertLocalRunInputReady, initLocalRunnerConfigFile, initLocalSpecFile, loadLocalRunInput, runRequestFromLocalSpec, } from "./local-project.js";
|
|
17
23
|
import { sanitizeLogLine } from "./run-reporter.js";
|
|
18
24
|
export * from "./compare.js";
|
|
19
25
|
export * from "./contracts.js";
|
|
20
26
|
export * from "./dataset.js";
|
|
21
27
|
export * from "./labeling.js";
|
|
22
28
|
export * from "./labeling-sanitize.js";
|
|
29
|
+
export * from "./model-server.js";
|
|
23
30
|
export * from "./orchestrator.js";
|
|
24
31
|
export * from "./local-project.js";
|
|
25
32
|
export * from "./openrouter.js";
|
|
33
|
+
export * from "./prefetch.js";
|
|
26
34
|
export * from "./run-reporter.js";
|
|
27
35
|
export * from "./server.js";
|
|
28
36
|
export * from "./store.js";
|
|
37
|
+
function packageVersion() {
|
|
38
|
+
try {
|
|
39
|
+
const packageJson = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
|
|
40
|
+
return typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "unknown";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const TT_LOCAL_VERSION = packageVersion();
|
|
29
47
|
export function getLocalRunnerInfo() {
|
|
30
48
|
return {
|
|
31
49
|
name: "tuned-tensor-local",
|
|
32
50
|
status: "local-runner-preview",
|
|
33
51
|
description: "Local fine-tuning runner for single-GPU uv/Python hosts.",
|
|
52
|
+
version: TT_LOCAL_VERSION,
|
|
34
53
|
};
|
|
35
54
|
}
|
|
36
55
|
/**
|
|
@@ -79,72 +98,343 @@ async function loadDotEnvFromCwd() {
|
|
|
79
98
|
}
|
|
80
99
|
function readOption(argv, name) {
|
|
81
100
|
const index = argv.indexOf(name);
|
|
82
|
-
if (index
|
|
83
|
-
return
|
|
84
|
-
|
|
101
|
+
if (index !== -1)
|
|
102
|
+
return argv[index + 1];
|
|
103
|
+
const inline = argv.find((value) => value.startsWith(`${name}=`));
|
|
104
|
+
return inline?.slice(name.length + 1);
|
|
85
105
|
}
|
|
86
106
|
function hasFlag(argv, name) {
|
|
87
107
|
return argv.includes(name);
|
|
88
108
|
}
|
|
89
109
|
function printHelp() {
|
|
90
|
-
console.log(`tt-local
|
|
110
|
+
console.log(`Usage: tt-local <command> [options]
|
|
91
111
|
|
|
92
112
|
Commands:
|
|
93
|
-
info
|
|
94
|
-
init [--name "My Local Model"] [--model Qwen/Qwen3.5-2B] [--output tunedtensor.json] [--force]
|
|
95
|
-
doctor [--config local-runner.json]
|
|
113
|
+
info Show package and runner information
|
|
114
|
+
init [--name "My Local Model"] [--model Qwen/Qwen3.5-2B] [--output tunedtensor.json] [--profile spark] [--force]
|
|
115
|
+
doctor [tunedtensor.json|request.json] [--config local-runner.json]
|
|
96
116
|
validate [tunedtensor.json|request.json] [--config local-runner.json]
|
|
97
117
|
run [tunedtensor.json|request.json] [--config local-runner.json] [--stage all|prepare|baseline|train|candidate|score|report]
|
|
98
118
|
[--run-id uuid] [--parent-model local-model-id] [--parent-model-artifact path-or-file-uri]
|
|
99
|
-
[--model-artifact path-or-file-uri] [--force] [--dry-run] [--verbose] [--quiet]
|
|
119
|
+
[--model-artifact path-or-file-uri] [--force] [--dry-run] [--detach] [--verbose] [--quiet]
|
|
100
120
|
label <input.jsonl|input.csv> [--input-column col] [--spec tunedtensor.json] [--system-prompt "..."]
|
|
101
121
|
[--model teacher-model] [--output labeled.jsonl] [--config local-runner.json] [--dry-run]
|
|
102
122
|
serve [--config local-runner.json] [--host 127.0.0.1] [--port 8787]
|
|
103
123
|
runs list|get|events|watch|report|compare|cancel|reconcile [args] [--config local-runner.json]
|
|
104
|
-
models list|get [args] [--config local-runner.json]
|
|
124
|
+
models list|get|verify|prefetch|verify-base|serve [args] [--config local-runner.json]
|
|
105
125
|
specs list|get|import [args] [--config local-runner.json]
|
|
106
126
|
store rebuild-index [--config local-runner.json]
|
|
107
127
|
|
|
128
|
+
Global options:
|
|
129
|
+
-h, --help Show help
|
|
130
|
+
-V, --version Show the installed version
|
|
131
|
+
|
|
108
132
|
The run command writes local artifacts under config.artifactRoot, defaulting to
|
|
109
133
|
.tt-local/artifacts. The file-backed local store defaults to
|
|
110
134
|
~/.tuned-tensor-local unless config.storeRoot or TT_LOCAL_HOME is set.`);
|
|
111
135
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
const CONFIG_OPTION = { name: "--config", value: "path", description: "Local runner config JSON path" };
|
|
137
|
+
const USER_ID_OPTION = { name: "--user-id", value: "id", description: "Override the local user ID" };
|
|
138
|
+
const RUN_NUMBER_OPTION = { name: "--run-number", value: "number", description: "Override the local run number" };
|
|
139
|
+
const VERBOSE_OPTION = { name: "--verbose", description: "Stream subprocess output" };
|
|
140
|
+
const QUIET_OPTION = { name: "--quiet", description: "Suppress progress output on stderr" };
|
|
141
|
+
const COMMAND_DEFINITIONS = {
|
|
142
|
+
info: {
|
|
143
|
+
usage: "tt-local info",
|
|
144
|
+
description: "Show the installed TT Local version and runner status.",
|
|
145
|
+
options: [],
|
|
146
|
+
maxPositionals: 0,
|
|
147
|
+
},
|
|
148
|
+
init: {
|
|
149
|
+
usage: "tt-local init [options]",
|
|
150
|
+
description: "Create a local tunedtensor.json behavior spec.",
|
|
151
|
+
options: [
|
|
152
|
+
{ name: "--name", value: "name", description: "Behavior spec name" },
|
|
153
|
+
{ name: "--model", value: "model", description: "Base model ID" },
|
|
154
|
+
{ name: "--output", value: "path", description: "Output spec path" },
|
|
155
|
+
{ name: "--profile", value: "profile", description: "Write a durable runner config (spark)" },
|
|
156
|
+
{ name: "--config-output", value: "path", description: "Profile config output path" },
|
|
157
|
+
{ name: "--force", description: "Overwrite an existing output file" },
|
|
158
|
+
],
|
|
159
|
+
maxPositionals: 0,
|
|
160
|
+
},
|
|
161
|
+
doctor: {
|
|
162
|
+
usage: "tt-local doctor [tunedtensor.json|request.json] [--config path]",
|
|
163
|
+
description: "Check the host and optional run input before starting work.",
|
|
164
|
+
options: [CONFIG_OPTION],
|
|
165
|
+
maxPositionals: 1,
|
|
166
|
+
},
|
|
167
|
+
validate: {
|
|
168
|
+
usage: "tt-local validate [tunedtensor.json|request.json] [options]",
|
|
169
|
+
description: "Validate a behavior spec or run request without executing it.",
|
|
170
|
+
options: [CONFIG_OPTION, USER_ID_OPTION, RUN_NUMBER_OPTION],
|
|
171
|
+
maxPositionals: 1,
|
|
172
|
+
},
|
|
173
|
+
run: {
|
|
174
|
+
usage: "tt-local run [tunedtensor.json|request.json] [options]",
|
|
175
|
+
description: "Run the local fine-tuning workflow.",
|
|
176
|
+
options: [
|
|
177
|
+
CONFIG_OPTION,
|
|
178
|
+
USER_ID_OPTION,
|
|
179
|
+
RUN_NUMBER_OPTION,
|
|
180
|
+
{ name: "--run-id", value: "uuid", description: "Resume or override a run ID" },
|
|
181
|
+
{ name: "--stage", value: "stage", description: "Run through a specific workflow stage" },
|
|
182
|
+
{ name: "--model-artifact", value: "uri", description: "Use an existing model artifact" },
|
|
183
|
+
{ name: "--parent-model", value: "model-id", description: "Continue training a stored local model" },
|
|
184
|
+
{ name: "--parent-model-artifact", value: "uri", description: "Continue training a model artifact" },
|
|
185
|
+
{ name: "--force", description: "Recompute the selected stage" },
|
|
186
|
+
{ name: "--dry-run", description: "Write representative artifacts without GPU work" },
|
|
187
|
+
{ name: "--detach", description: "Run in the background and return the run ID immediately" },
|
|
188
|
+
VERBOSE_OPTION,
|
|
189
|
+
QUIET_OPTION,
|
|
190
|
+
],
|
|
191
|
+
maxPositionals: 1,
|
|
192
|
+
},
|
|
193
|
+
label: {
|
|
194
|
+
usage: "tt-local label <input.jsonl|input.csv> [options]",
|
|
195
|
+
description: "Label local rows with a teacher model.",
|
|
196
|
+
options: [
|
|
197
|
+
CONFIG_OPTION,
|
|
198
|
+
{ name: "--input-column", value: "column", description: "CSV input column" },
|
|
199
|
+
{ name: "--spec", value: "path", description: "Behavior spec path" },
|
|
200
|
+
{ name: "--system-prompt", value: "text", description: "Explicit teacher system prompt" },
|
|
201
|
+
{ name: "--model", value: "model", description: "Teacher model ID" },
|
|
202
|
+
{ name: "--output", value: "path", description: "Output JSONL path" },
|
|
203
|
+
{ name: "--format", value: "jsonl|csv", description: "Force the source format" },
|
|
204
|
+
{ name: "--dry-run", description: "Parse and sanitize without teacher calls" },
|
|
205
|
+
VERBOSE_OPTION,
|
|
206
|
+
QUIET_OPTION,
|
|
207
|
+
],
|
|
208
|
+
minPositionals: 1,
|
|
209
|
+
maxPositionals: 1,
|
|
210
|
+
missingPositionalsMessage: "label requires <input.jsonl|input.csv>",
|
|
211
|
+
},
|
|
212
|
+
serve: {
|
|
213
|
+
usage: "tt-local serve [--config path] [--host host] [--port port]",
|
|
214
|
+
description: "Serve the local runs dashboard.",
|
|
215
|
+
options: [
|
|
216
|
+
CONFIG_OPTION,
|
|
217
|
+
{ name: "--host", value: "host", description: "Dashboard bind host" },
|
|
218
|
+
{ name: "--port", value: "port", description: "Dashboard bind port" },
|
|
219
|
+
],
|
|
220
|
+
maxPositionals: 0,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
const COMMAND_GROUPS = {
|
|
224
|
+
runs: {
|
|
225
|
+
description: "Inspect and manage locally stored runs.",
|
|
226
|
+
defaultSubcommand: "list",
|
|
227
|
+
subcommands: {
|
|
228
|
+
list: { usage: "tt-local runs list [--config path]", description: "List local runs.", options: [CONFIG_OPTION], maxPositionals: 0 },
|
|
229
|
+
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>" },
|
|
230
|
+
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>" },
|
|
231
|
+
watch: { usage: "tt-local runs watch <run-id> [--config path]", description: "Watch a run until it finishes.", options: [CONFIG_OPTION], minPositionals: 1, maxPositionals: 1, missingPositionalsMessage: "runs watch requires <run-id>" },
|
|
232
|
+
report: { usage: "tt-local runs report <run-id> [--config path]", description: "Get a run comparison report.", options: [CONFIG_OPTION], minPositionals: 1, maxPositionals: 1, missingPositionalsMessage: "runs report requires <run-id>" },
|
|
233
|
+
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>" },
|
|
234
|
+
cancel: { usage: "tt-local runs cancel <run-id> [--config path]", description: "Request cancellation of a run.", options: [CONFIG_OPTION], minPositionals: 1, maxPositionals: 1, missingPositionalsMessage: "runs cancel requires <run-id>" },
|
|
235
|
+
reconcile: { usage: "tt-local runs reconcile [--config path]", description: "Rebuild local store indexes.", options: [CONFIG_OPTION], maxPositionals: 0 },
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
models: {
|
|
239
|
+
description: "Inspect, verify, prefetch, or serve local models.",
|
|
240
|
+
defaultSubcommand: "list",
|
|
241
|
+
subcommands: {
|
|
242
|
+
list: { usage: "tt-local models list [--config path]", description: "List local models.", options: [CONFIG_OPTION], maxPositionals: 0 },
|
|
243
|
+
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>" },
|
|
244
|
+
verify: {
|
|
245
|
+
usage: "tt-local models verify <model-id-or-artifact-path> [--config path]",
|
|
246
|
+
description: "Verify a stored model or manifested artifact path.",
|
|
247
|
+
options: [CONFIG_OPTION],
|
|
248
|
+
minPositionals: 1,
|
|
249
|
+
maxPositionals: 1,
|
|
250
|
+
missingPositionalsMessage: "models verify requires <model-id-or-artifact-path>",
|
|
251
|
+
},
|
|
252
|
+
prefetch: {
|
|
253
|
+
usage: "tt-local models prefetch [tunedtensor.json|request.json] [options]",
|
|
254
|
+
description: "Download the configured base model before a run.",
|
|
255
|
+
options: [CONFIG_OPTION, USER_ID_OPTION, RUN_NUMBER_OPTION, VERBOSE_OPTION, QUIET_OPTION],
|
|
256
|
+
maxPositionals: 1,
|
|
257
|
+
},
|
|
258
|
+
"verify-base": {
|
|
259
|
+
usage: "tt-local models verify-base [tunedtensor.json|request.json] [options]",
|
|
260
|
+
description: "Verify that the configured base-model snapshot is complete and locally available.",
|
|
261
|
+
options: [CONFIG_OPTION, USER_ID_OPTION, RUN_NUMBER_OPTION, VERBOSE_OPTION, QUIET_OPTION],
|
|
262
|
+
maxPositionals: 1,
|
|
263
|
+
},
|
|
264
|
+
serve: {
|
|
265
|
+
usage: "tt-local models serve <model-id> [options]",
|
|
266
|
+
description: "Serve a verified adapter through an OpenAI-compatible local API.",
|
|
267
|
+
options: [
|
|
268
|
+
CONFIG_OPTION,
|
|
269
|
+
{ name: "--host", value: "host", description: "Bind host (localhost by default)" },
|
|
270
|
+
{ name: "--port", value: "port", description: "Bind port" },
|
|
271
|
+
{ name: "--device", value: "device", description: "auto, cpu, cuda, or mps" },
|
|
272
|
+
{ name: "--max-tokens", value: "count", description: "Default response token limit" },
|
|
273
|
+
{ name: "--temperature", value: "number", description: "Default sampling temperature" },
|
|
274
|
+
{ name: "--top-p", value: "number", description: "Default nucleus sampling threshold" },
|
|
275
|
+
{ name: "--max-concurrent-requests", value: "count", description: "Concurrent generation limit" },
|
|
276
|
+
{ name: "--spec", value: "path", description: "Behavior spec whose instructions are enforced" },
|
|
277
|
+
{ name: "--no-spec-prompt", description: "Do not enforce the stored behavior-spec prompt" },
|
|
278
|
+
{ name: "--allow-remote", description: "Allow a non-loopback bind" },
|
|
279
|
+
{ name: "--api-key-env", value: "name", description: "Environment variable containing a bearer token" },
|
|
280
|
+
{ name: "--print-command", description: "Validate and print the launch plan without starting" },
|
|
281
|
+
],
|
|
282
|
+
minPositionals: 1,
|
|
283
|
+
maxPositionals: 1,
|
|
284
|
+
missingPositionalsMessage: "models serve requires <model-id>",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
specs: {
|
|
289
|
+
description: "Inspect and import local behavior specs.",
|
|
290
|
+
defaultSubcommand: "list",
|
|
291
|
+
subcommands: {
|
|
292
|
+
list: { usage: "tt-local specs list [--config path]", description: "List local behavior specs.", options: [CONFIG_OPTION], maxPositionals: 0 },
|
|
293
|
+
get: { usage: "tt-local specs get <spec-id> [--config path]", description: "Get a local behavior spec.", options: [CONFIG_OPTION], minPositionals: 1, maxPositionals: 1, missingPositionalsMessage: "specs get requires <spec-id>" },
|
|
294
|
+
import: {
|
|
295
|
+
usage: "tt-local specs import <spec-or-request.json> [options]",
|
|
296
|
+
description: "Import a behavior spec into the local store.",
|
|
297
|
+
options: [CONFIG_OPTION, USER_ID_OPTION, RUN_NUMBER_OPTION, { name: "--id", value: "id", description: "ID for a raw spec snapshot" }],
|
|
298
|
+
minPositionals: 1,
|
|
299
|
+
maxPositionals: 1,
|
|
300
|
+
missingPositionalsMessage: "specs import requires <spec-or-request.json>",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
store: {
|
|
305
|
+
description: "Maintain the local file-backed store.",
|
|
306
|
+
subcommands: {
|
|
307
|
+
"rebuild-index": { usage: "tt-local store rebuild-index [--config path]", description: "Rebuild store indexes from durable records.", options: [CONFIG_OPTION], maxPositionals: 0 },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
function hasHelpFlag(argv) {
|
|
312
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
313
|
+
}
|
|
314
|
+
function printCommandHelp(definition) {
|
|
315
|
+
console.log(`Usage: ${definition.usage}\n\n${definition.description}`);
|
|
316
|
+
if (definition.options.length > 0) {
|
|
317
|
+
console.log("\nOptions:");
|
|
318
|
+
for (const option of definition.options) {
|
|
319
|
+
const label = option.value ? `${option.name} <${option.value}>` : option.name;
|
|
320
|
+
console.log(` ${label.padEnd(34)} ${option.description}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
console.log(" -h, --help Show help");
|
|
324
|
+
}
|
|
325
|
+
function printGroupHelp(command, group) {
|
|
326
|
+
console.log(`Usage: tt-local ${command} <command> [options]\n\n${group.description}\n\nCommands:`);
|
|
327
|
+
for (const [name, definition] of Object.entries(group.subcommands)) {
|
|
328
|
+
console.log(` ${name.padEnd(16)} ${definition.description}`);
|
|
329
|
+
}
|
|
330
|
+
console.log("\nRun `tt-local " + command + " <command> --help` for command-specific help.");
|
|
331
|
+
}
|
|
332
|
+
function parseCommandArguments(tokens, definition) {
|
|
333
|
+
const options = new Map(definition.options.map((option) => [option.name, option]));
|
|
334
|
+
const seen = new Set();
|
|
133
335
|
const positionals = [];
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
index += 1;
|
|
336
|
+
let optionsEnded = false;
|
|
337
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
338
|
+
const token = tokens[index];
|
|
339
|
+
if (!optionsEnded && token === "--") {
|
|
340
|
+
optionsEnded = true;
|
|
140
341
|
continue;
|
|
141
342
|
}
|
|
142
|
-
if (
|
|
343
|
+
if (!optionsEnded && token.startsWith("-")) {
|
|
344
|
+
const equalsIndex = token.indexOf("=");
|
|
345
|
+
const name = equalsIndex === -1 ? token : token.slice(0, equalsIndex);
|
|
346
|
+
const inlineValue = equalsIndex === -1 ? undefined : token.slice(equalsIndex + 1);
|
|
347
|
+
const option = options.get(name);
|
|
348
|
+
if (!option)
|
|
349
|
+
throw new Error(`Unknown option: ${name}`);
|
|
350
|
+
if (seen.has(name))
|
|
351
|
+
throw new Error(`Option ${name} may only be specified once.`);
|
|
352
|
+
seen.add(name);
|
|
353
|
+
if (option.value) {
|
|
354
|
+
if (inlineValue !== undefined) {
|
|
355
|
+
if (!inlineValue)
|
|
356
|
+
throw new Error(`Option ${name} requires a value.`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const value = tokens[index + 1];
|
|
360
|
+
if (value === undefined || value.startsWith("-")) {
|
|
361
|
+
throw new Error(`Option ${name} requires a value.`);
|
|
362
|
+
}
|
|
363
|
+
index += 1;
|
|
364
|
+
}
|
|
365
|
+
else if (inlineValue !== undefined) {
|
|
366
|
+
throw new Error(`Option ${name} does not accept a value.`);
|
|
367
|
+
}
|
|
143
368
|
continue;
|
|
144
|
-
|
|
369
|
+
}
|
|
370
|
+
positionals.push(token);
|
|
371
|
+
}
|
|
372
|
+
if (positionals.length < (definition.minPositionals ?? 0)) {
|
|
373
|
+
throw new Error(definition.missingPositionalsMessage ?? `Missing required argument. Usage: ${definition.usage}`);
|
|
374
|
+
}
|
|
375
|
+
if (definition.maxPositionals !== undefined && positionals.length > definition.maxPositionals) {
|
|
376
|
+
throw new Error(`Too many arguments. Usage: ${definition.usage}`);
|
|
145
377
|
}
|
|
146
378
|
return positionals;
|
|
147
379
|
}
|
|
380
|
+
function parseCli(argv) {
|
|
381
|
+
const command = argv[2] ?? "info";
|
|
382
|
+
if (command === "--help" || command === "-h") {
|
|
383
|
+
return { command: "info", positionals: [], help: "top" };
|
|
384
|
+
}
|
|
385
|
+
if (command === "--version" || command === "-V") {
|
|
386
|
+
if (argv.length > 3)
|
|
387
|
+
throw new Error(`${command} does not accept arguments.`);
|
|
388
|
+
return { command, positionals: [], help: "command" };
|
|
389
|
+
}
|
|
390
|
+
if (command.startsWith("-"))
|
|
391
|
+
throw new Error(`Unknown option: ${command}`);
|
|
392
|
+
const definition = COMMAND_DEFINITIONS[command];
|
|
393
|
+
if (definition) {
|
|
394
|
+
if (hasHelpFlag(argv.slice(3))) {
|
|
395
|
+
return { command, positionals: [], help: "command", definition };
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
command,
|
|
399
|
+
positionals: parseCommandArguments(argv.slice(3), definition),
|
|
400
|
+
help: "top",
|
|
401
|
+
definition,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const group = COMMAND_GROUPS[command];
|
|
405
|
+
if (!group)
|
|
406
|
+
throw new Error(`Unknown command: ${command}`);
|
|
407
|
+
if (argv[3] === "--help" || argv[3] === "-h") {
|
|
408
|
+
return { command, positionals: [], help: "group" };
|
|
409
|
+
}
|
|
410
|
+
const candidate = argv[3];
|
|
411
|
+
let subcommand;
|
|
412
|
+
let tokenStart;
|
|
413
|
+
if (candidate && !candidate.startsWith("-")) {
|
|
414
|
+
subcommand = candidate;
|
|
415
|
+
tokenStart = 4;
|
|
416
|
+
}
|
|
417
|
+
else if (group.defaultSubcommand) {
|
|
418
|
+
subcommand = group.defaultSubcommand;
|
|
419
|
+
tokenStart = 3;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
throw new Error(`${command} requires a subcommand. Run 'tt-local ${command} --help'.`);
|
|
423
|
+
}
|
|
424
|
+
const subcommandDefinition = group.subcommands[subcommand];
|
|
425
|
+
if (!subcommandDefinition)
|
|
426
|
+
throw new Error(`Unknown ${command} command: ${subcommand}`);
|
|
427
|
+
if (hasHelpFlag(argv.slice(tokenStart))) {
|
|
428
|
+
return { command, subcommand, positionals: [], help: "command", definition: subcommandDefinition };
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
command,
|
|
432
|
+
subcommand,
|
|
433
|
+
positionals: parseCommandArguments(argv.slice(tokenStart), subcommandDefinition),
|
|
434
|
+
help: "top",
|
|
435
|
+
definition: subcommandDefinition,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
148
438
|
function readNumberOption(argv, name) {
|
|
149
439
|
const value = readOption(argv, name);
|
|
150
440
|
return value ? Number(value) : undefined;
|
|
@@ -233,10 +523,45 @@ async function parentModelSelectionFromArgv(argv, config) {
|
|
|
233
523
|
return undefined;
|
|
234
524
|
const store = createLocalStore(config.storeRoot);
|
|
235
525
|
const record = await store.getModel(parentModel);
|
|
526
|
+
const verified = await verifyStoredModel(record, { requireServable: true });
|
|
527
|
+
const contract = verified.contract;
|
|
528
|
+
const baseModelRevision = typeof contract.base_model_revision === "string"
|
|
529
|
+
? contract.base_model_revision
|
|
530
|
+
: undefined;
|
|
531
|
+
const baseModelArtifactUri = typeof contract.base_model_artifact_uri === "string"
|
|
532
|
+
? contract.base_model_artifact_uri
|
|
533
|
+
: undefined;
|
|
534
|
+
const baseModelFingerprint = typeof contract.base_model_fingerprint === "string"
|
|
535
|
+
? contract.base_model_fingerprint
|
|
536
|
+
: undefined;
|
|
537
|
+
if (baseModelArtifactUri && baseModelFingerprint) {
|
|
538
|
+
if (!config.paths.baseModel) {
|
|
539
|
+
throw new Error(`Parent model ${record.id} was trained from a local base snapshot. Configure paths.baseModel to the same `
|
|
540
|
+
+ "snapshot before continuing it.");
|
|
541
|
+
}
|
|
542
|
+
const configuredFingerprint = await fingerprintLocalBaseModel(config.paths.baseModel);
|
|
543
|
+
if (configuredFingerprint !== baseModelFingerprint) {
|
|
544
|
+
throw new Error(`Configured paths.baseModel content does not match parent model ${record.id}.`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else if (!baseModelRevision) {
|
|
548
|
+
throw new Error(`Parent model ${record.id} does not record an immutable base-model revision or local snapshot fingerprint. `
|
|
549
|
+
+ "Use --parent-model-artifact only if you intentionally accept responsibility for that unpinned artifact.");
|
|
550
|
+
}
|
|
551
|
+
if (baseModelRevision && config.paths.baseModel) {
|
|
552
|
+
const snapshotRevision = resolve(config.paths.baseModel).match(/[\\/]snapshots[\\/]([^\\/]+)(?:[\\/]|$)/)?.[1];
|
|
553
|
+
if (!baseModelFingerprint && snapshotRevision !== baseModelRevision) {
|
|
554
|
+
throw new Error(`Parent model ${record.id} requires base revision ${baseModelRevision}; configured paths.baseModel does not `
|
|
555
|
+
+ "identify that snapshot revision.");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
236
558
|
return {
|
|
237
559
|
artifactUri: record.artifact_uri,
|
|
238
560
|
modelId: record.id,
|
|
239
561
|
baseModel: record.base_model,
|
|
562
|
+
baseModelRevision,
|
|
563
|
+
baseModelArtifactUri,
|
|
564
|
+
baseModelFingerprint,
|
|
240
565
|
};
|
|
241
566
|
}
|
|
242
567
|
function withParentModelArtifact(request, parentModel) {
|
|
@@ -246,10 +571,16 @@ function withParentModelArtifact(request, parentModel) {
|
|
|
246
571
|
throw new Error(`Parent model ${parentModel.modelId ?? ""} uses base model ${parentModel.baseModel}, `
|
|
247
572
|
+ `but this run uses ${request.spec_snapshot.base_model}. Continue from a model with the same base model.`);
|
|
248
573
|
}
|
|
574
|
+
const requestedRevision = request.hyperparameters.base_model_revision;
|
|
575
|
+
if (parentModel.baseModelRevision && requestedRevision && parentModel.baseModelRevision !== requestedRevision) {
|
|
576
|
+
throw new Error(`Parent model ${parentModel.modelId ?? ""} uses base revision ${parentModel.baseModelRevision}, `
|
|
577
|
+
+ `but this run requests ${requestedRevision}.`);
|
|
578
|
+
}
|
|
249
579
|
return fineTuneRunRequestSchema.parse({
|
|
250
580
|
...request,
|
|
251
581
|
hyperparameters: {
|
|
252
582
|
...request.hyperparameters,
|
|
583
|
+
...(parentModel.baseModelRevision ? { base_model_revision: parentModel.baseModelRevision } : {}),
|
|
253
584
|
parent_model_artifact: parentModel.artifactUri,
|
|
254
585
|
},
|
|
255
586
|
});
|
|
@@ -257,26 +588,265 @@ function withParentModelArtifact(request, parentModel) {
|
|
|
257
588
|
async function sleep(ms) {
|
|
258
589
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
259
590
|
}
|
|
260
|
-
function
|
|
261
|
-
|
|
591
|
+
function detachedRunArgv(argv, runId) {
|
|
592
|
+
const tokens = argv.slice(3);
|
|
593
|
+
const output = [];
|
|
594
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
595
|
+
const token = tokens[index];
|
|
596
|
+
if (token === "--detach")
|
|
597
|
+
continue;
|
|
598
|
+
if (token === "--run-id") {
|
|
599
|
+
index += 1;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (token.startsWith("--run-id="))
|
|
603
|
+
continue;
|
|
604
|
+
output.push(token);
|
|
605
|
+
}
|
|
606
|
+
return ["run", ...output, "--run-id", runId];
|
|
607
|
+
}
|
|
608
|
+
async function launchDetachedRun(args) {
|
|
609
|
+
if (!process.argv[1])
|
|
610
|
+
throw new Error("Cannot determine the tt-local CLI entrypoint for --detach.");
|
|
611
|
+
const prefix = args.request.artifacts?.prefix ?? defaultArtifactPrefix({
|
|
612
|
+
userId: args.request.user_id,
|
|
613
|
+
behaviorSpecId: args.request.behavior_spec_id,
|
|
614
|
+
runId: args.request.run_id,
|
|
615
|
+
});
|
|
616
|
+
const artifacts = resolveRunArtifacts({ artifactRoot: args.config.artifactRoot, prefix });
|
|
617
|
+
await claimRunArtifactDirectory({
|
|
618
|
+
artifacts,
|
|
619
|
+
runId: args.request.run_id,
|
|
620
|
+
userId: args.request.user_id,
|
|
621
|
+
behaviorSpecId: args.request.behavior_spec_id,
|
|
622
|
+
});
|
|
623
|
+
const store = createLocalStore(args.config.storeRoot);
|
|
624
|
+
const existing = await store.getRun(args.request.run_id).catch(() => null);
|
|
625
|
+
if (existing) {
|
|
626
|
+
await store.syncRunRequest(args.request);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
await store.startRun({ request: args.request, artifactDir: artifacts.runDir });
|
|
630
|
+
}
|
|
631
|
+
const logPath = resolve(artifacts.runDir, "detached.log");
|
|
632
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
633
|
+
const log = await open(logPath, "a");
|
|
634
|
+
try {
|
|
635
|
+
const child = spawn(process.execPath, [
|
|
636
|
+
...process.execArgv,
|
|
637
|
+
process.argv[1],
|
|
638
|
+
...detachedRunArgv(args.argv, args.request.run_id),
|
|
639
|
+
], {
|
|
640
|
+
detached: true,
|
|
641
|
+
stdio: ["ignore", log.fd, log.fd],
|
|
642
|
+
env: { ...process.env, TT_LOCAL_DETACHED: "1" },
|
|
643
|
+
});
|
|
644
|
+
await new Promise((resolveSpawn, reject) => {
|
|
645
|
+
child.once("spawn", resolveSpawn);
|
|
646
|
+
child.once("error", reject);
|
|
647
|
+
});
|
|
648
|
+
if (!child.pid)
|
|
649
|
+
throw new Error("Detached tt-local process did not report a PID.");
|
|
650
|
+
child.unref();
|
|
651
|
+
return { pid: child.pid, logPath, artifactDir: artifacts.runDir };
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
await store.failRun(args.request.run_id, `Failed to launch detached workflow: ${error instanceof Error ? error.message : String(error)}`).catch(() => undefined);
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
await log.close();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async function verifyStoredModel(model, options = {}) {
|
|
662
|
+
const manifestPath = join(model.artifact_dir, "artifact-manifest.json");
|
|
663
|
+
const integrity = await assertArtifactManifest(manifestPath, {
|
|
664
|
+
requiredPaths: ["stage-metadata.json", "training-report.json"],
|
|
665
|
+
scopeToRequired: true,
|
|
666
|
+
verifyModel: true,
|
|
667
|
+
});
|
|
668
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
669
|
+
if (!manifest.model) {
|
|
670
|
+
throw new Error(`Artifact manifest does not contain a model contract: ${manifestPath}`);
|
|
671
|
+
}
|
|
672
|
+
const customNonServable = manifest.model.framework !== "transformers-peft"
|
|
673
|
+
&& manifest.model.servable === false;
|
|
674
|
+
const artifact = await assertUsableModelArtifact(model.artifact_uri, {
|
|
675
|
+
allowUnrecognizedPayload: customNonServable && !options.requireServable,
|
|
676
|
+
});
|
|
677
|
+
if (typeof manifest.model.base_model_artifact_uri === "string"
|
|
678
|
+
&& typeof manifest.model.base_model_fingerprint === "string") {
|
|
679
|
+
const actualBaseFingerprint = await fingerprintLocalBaseModel(manifest.model.base_model_artifact_uri);
|
|
680
|
+
if (actualBaseFingerprint !== manifest.model.base_model_fingerprint) {
|
|
681
|
+
throw new Error("Recorded local base-model content no longer matches the model artifact contract.");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (typeof manifest.model.artifact_root !== "string"
|
|
685
|
+
|| resolve(manifest.model.artifact_root) !== resolve(artifact.path)) {
|
|
686
|
+
throw new Error("Stored model record does not match the artifact covered by its manifest.");
|
|
687
|
+
}
|
|
688
|
+
if (manifest.model.base_model !== model.base_model) {
|
|
689
|
+
throw new Error("Stored model base model does not match its artifact manifest.");
|
|
690
|
+
}
|
|
691
|
+
if (options.requireServable && manifest.model.servable !== true) {
|
|
692
|
+
throw new Error(`Model ${model.id} is valid but its artifact contract does not mark it as locally servable.`);
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
manifest_path: manifestPath,
|
|
696
|
+
integrity,
|
|
697
|
+
artifact,
|
|
698
|
+
contract: manifest.model,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
async function verifyModelArtifactPath(input) {
|
|
702
|
+
const inputPath = resolve(input);
|
|
703
|
+
let manifestPath;
|
|
704
|
+
let artifactUri = inputPath;
|
|
705
|
+
if (basename(inputPath) === "artifact-manifest.json") {
|
|
706
|
+
manifestPath = inputPath;
|
|
707
|
+
const raw = JSON.parse(await readFile(inputPath, "utf8"));
|
|
708
|
+
if (typeof raw.model?.artifact_root !== "string") {
|
|
709
|
+
throw new Error(`Artifact manifest does not contain a model contract: ${inputPath}`);
|
|
710
|
+
}
|
|
711
|
+
artifactUri = raw.model.artifact_root;
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
const metadata = await stat(inputPath).catch(() => null);
|
|
715
|
+
if (!metadata)
|
|
716
|
+
throw new Error(`Model not found and artifact path does not exist: ${input}`);
|
|
717
|
+
let current = metadata.isDirectory() ? inputPath : dirname(inputPath);
|
|
718
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
719
|
+
const candidate = join(current, "artifact-manifest.json");
|
|
720
|
+
const raw = await readFile(candidate, "utf8").catch(() => null);
|
|
721
|
+
if (raw) {
|
|
722
|
+
const parsed = JSON.parse(raw);
|
|
723
|
+
if (typeof parsed.model?.artifact_root === "string"
|
|
724
|
+
&& resolve(parsed.model.artifact_root) === resolve(inputPath)) {
|
|
725
|
+
manifestPath = candidate;
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const parent = dirname(current);
|
|
730
|
+
if (parent === current)
|
|
731
|
+
break;
|
|
732
|
+
current = parent;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (!manifestPath) {
|
|
736
|
+
throw new Error(`No artifact manifest covering model path ${inputPath} was found in its parent run directory.`);
|
|
737
|
+
}
|
|
738
|
+
const integrity = await assertArtifactManifest(manifestPath, {
|
|
739
|
+
requiredPaths: ["stage-metadata.json", "training-report.json"],
|
|
740
|
+
scopeToRequired: true,
|
|
741
|
+
verifyModel: true,
|
|
742
|
+
});
|
|
743
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
744
|
+
if (!manifest.model)
|
|
745
|
+
throw new Error(`Artifact manifest does not contain a model contract: ${manifestPath}`);
|
|
746
|
+
const customNonServable = manifest.model.framework !== "transformers-peft"
|
|
747
|
+
&& manifest.model.servable === false;
|
|
748
|
+
const artifact = await assertUsableModelArtifact(artifactUri, {
|
|
749
|
+
allowUnrecognizedPayload: customNonServable,
|
|
750
|
+
});
|
|
751
|
+
if (typeof manifest.model.base_model_artifact_uri === "string"
|
|
752
|
+
&& typeof manifest.model.base_model_fingerprint === "string") {
|
|
753
|
+
const actualBaseFingerprint = await fingerprintLocalBaseModel(manifest.model.base_model_artifact_uri);
|
|
754
|
+
if (actualBaseFingerprint !== manifest.model.base_model_fingerprint) {
|
|
755
|
+
throw new Error("Recorded local base-model content no longer matches the model artifact contract.");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (typeof manifest.model.artifact_root !== "string"
|
|
759
|
+
|| resolve(manifest.model.artifact_root) !== resolve(artifact.path)) {
|
|
760
|
+
throw new Error("Model path does not match the artifact covered by its manifest.");
|
|
761
|
+
}
|
|
762
|
+
return { manifest_path: manifestPath, integrity, artifact, contract: manifest.model };
|
|
763
|
+
}
|
|
764
|
+
async function modelSystemPrompt(args) {
|
|
765
|
+
const specPath = readOption(args.argv, "--spec");
|
|
766
|
+
if (specPath && hasFlag(args.argv, "--no-spec-prompt")) {
|
|
767
|
+
throw new Error("Use only one of --spec or --no-spec-prompt.");
|
|
768
|
+
}
|
|
769
|
+
if (hasFlag(args.argv, "--no-spec-prompt"))
|
|
770
|
+
return undefined;
|
|
771
|
+
let spec;
|
|
772
|
+
if (specPath) {
|
|
773
|
+
const input = JSON.parse(await readFile(resolve(specPath), "utf8"));
|
|
774
|
+
const local = localBehaviorSpecFileSchema.safeParse(input);
|
|
775
|
+
const request = fineTuneRunRequestSchema.safeParse(input);
|
|
776
|
+
const snapshot = specSnapshotSchema.safeParse(input);
|
|
777
|
+
const parsedSpec = local.success ? local.data : request.success ? request.data.spec_snapshot : snapshot.success ? snapshot.data : null;
|
|
778
|
+
if (!parsedSpec)
|
|
779
|
+
throw new Error(`--spec must contain a TT Local behavior spec or run request: ${resolve(specPath)}`);
|
|
780
|
+
spec = parsedSpec;
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
const runRequestPath = join(args.store.paths.runsDir, args.model.run_id, "request.json");
|
|
784
|
+
let persistedRequest = null;
|
|
785
|
+
try {
|
|
786
|
+
persistedRequest = fineTuneRunRequestSchema.parse(JSON.parse(await readFile(runRequestPath, "utf8")));
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
if (error.code !== "ENOENT")
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
792
|
+
spec = persistedRequest?.spec_snapshot
|
|
793
|
+
?? (await args.store.getSpec(args.model.behavior_spec_id)).spec;
|
|
794
|
+
}
|
|
795
|
+
if (spec.base_model !== args.model.base_model) {
|
|
796
|
+
throw new Error(`Behavior spec base model ${spec.base_model} does not match stored model base ${args.model.base_model}.`);
|
|
797
|
+
}
|
|
798
|
+
const prompt = buildSystemMessage(spec);
|
|
799
|
+
const metadata = JSON.parse(await readFile(join(args.model.artifact_dir, "stage-metadata.json"), "utf8"));
|
|
800
|
+
const promptHash = createHash("sha256").update(prompt).digest("hex");
|
|
801
|
+
if (metadata.system_prompt_sha256 !== promptHash) {
|
|
802
|
+
throw new Error("Behavior spec instructions do not match the prompt fingerprint used for this trained model. "
|
|
803
|
+
+ "Pass the original --spec, or use --no-spec-prompt only when this change is intentional.");
|
|
804
|
+
}
|
|
805
|
+
return prompt;
|
|
806
|
+
}
|
|
807
|
+
function modelServeDevice(argv) {
|
|
808
|
+
const value = readOption(argv, "--device");
|
|
809
|
+
if (value === undefined || value === "auto" || value === "cpu" || value === "cuda" || value === "mps") {
|
|
810
|
+
return value;
|
|
811
|
+
}
|
|
812
|
+
throw new Error(`--device must be one of auto, cpu, cuda, mps; got: ${value}`);
|
|
262
813
|
}
|
|
263
814
|
async function main(argv) {
|
|
264
|
-
const
|
|
815
|
+
const cli = parseCli(argv);
|
|
816
|
+
const command = cli.command;
|
|
817
|
+
if (command === "--version" || command === "-V") {
|
|
818
|
+
console.log(TT_LOCAL_VERSION);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (cli.help === "top" && (argv[2] === "--help" || argv[2] === "-h")) {
|
|
822
|
+
printHelp();
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (cli.help === "group") {
|
|
826
|
+
printGroupHelp(command, COMMAND_GROUPS[command]);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (cli.help === "command" && cli.definition) {
|
|
830
|
+
printCommandHelp(cli.definition);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
265
833
|
const loadedEnv = await loadDotEnvFromCwd();
|
|
266
834
|
if (loadedEnv.length > 0 && !hasFlag(argv, "--quiet")) {
|
|
267
835
|
process.stderr.write(`[tt-local] loaded ${loadedEnv.join(", ")} from .env\n`);
|
|
268
836
|
}
|
|
269
|
-
if (command === "info"
|
|
837
|
+
if (command === "info") {
|
|
270
838
|
const info = getLocalRunnerInfo();
|
|
271
839
|
console.log(`${info.name}: ${info.description}`);
|
|
840
|
+
console.log(`Version: ${info.version}`);
|
|
272
841
|
console.log(`Status: ${info.status}`);
|
|
273
|
-
if (command !== "info")
|
|
274
|
-
printHelp();
|
|
275
842
|
return;
|
|
276
843
|
}
|
|
277
844
|
if (command === "doctor") {
|
|
278
845
|
const config = await configFromArgv(argv);
|
|
279
|
-
const
|
|
846
|
+
const request = cli.positionals[0]
|
|
847
|
+
? (await loadLocalRunInput(resolve(cli.positionals[0]))).request
|
|
848
|
+
: undefined;
|
|
849
|
+
const checks = await runDoctor(config, request);
|
|
280
850
|
const ok = checks.every((check) => check.ok);
|
|
281
851
|
printJson({ ok, checks });
|
|
282
852
|
if (!ok)
|
|
@@ -285,35 +855,52 @@ async function main(argv) {
|
|
|
285
855
|
}
|
|
286
856
|
if (command === "init") {
|
|
287
857
|
const outputPath = resolve(readOption(argv, "--output") ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
858
|
+
const profile = readOption(argv, "--profile");
|
|
859
|
+
if (profile !== undefined && profile !== "spark") {
|
|
860
|
+
throw new Error(`--profile must be spark, got: ${profile}`);
|
|
861
|
+
}
|
|
288
862
|
const spec = await initLocalSpecFile({
|
|
289
863
|
outputPath,
|
|
290
864
|
name: readOption(argv, "--name") ?? "Local Tuned Tensor Spec",
|
|
291
865
|
baseModel: readOption(argv, "--model") ?? "Qwen/Qwen3.5-2B",
|
|
292
866
|
force: hasFlag(argv, "--force"),
|
|
293
867
|
});
|
|
868
|
+
const configPath = profile
|
|
869
|
+
? resolve(readOption(argv, "--config-output") ?? resolve(dirname(outputPath), "local-runner.json"))
|
|
870
|
+
: undefined;
|
|
871
|
+
if (profile && configPath) {
|
|
872
|
+
await initLocalRunnerConfigFile({
|
|
873
|
+
outputPath: configPath,
|
|
874
|
+
profile,
|
|
875
|
+
force: hasFlag(argv, "--force"),
|
|
876
|
+
});
|
|
877
|
+
}
|
|
294
878
|
printJson({
|
|
295
879
|
ok: true,
|
|
296
880
|
path: outputPath,
|
|
297
881
|
id: spec.id,
|
|
298
882
|
name: spec.name,
|
|
299
883
|
base_model: spec.base_model,
|
|
884
|
+
config_path: configPath ?? null,
|
|
300
885
|
});
|
|
301
886
|
return;
|
|
302
887
|
}
|
|
303
888
|
if (command === "validate") {
|
|
304
|
-
const inputPath = resolve(
|
|
889
|
+
const inputPath = resolve(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
305
890
|
const input = await loadLocalRunInput(inputPath, {
|
|
306
891
|
userId: readOption(argv, "--user-id"),
|
|
307
892
|
runNumber: readNumberOption(argv, "--run-number"),
|
|
308
893
|
});
|
|
309
894
|
const request = input.request;
|
|
310
895
|
const config = await configFromArgv(argv);
|
|
896
|
+
assertLocalRunInputReady(request);
|
|
311
897
|
assertSupportedValidateShape(request, config);
|
|
898
|
+
assertEvaluationScoringReady(config);
|
|
312
899
|
printJson({
|
|
313
900
|
ok: true,
|
|
314
901
|
input_kind: input.kind,
|
|
315
902
|
input_path: input.path,
|
|
316
|
-
run_id: request.run_id,
|
|
903
|
+
run_id: input.kind === "request" ? request.run_id : null,
|
|
317
904
|
behavior_spec_id: request.behavior_spec_id,
|
|
318
905
|
training_method: request.training_method,
|
|
319
906
|
base_model: request.spec_snapshot.base_model,
|
|
@@ -326,7 +913,7 @@ async function main(argv) {
|
|
|
326
913
|
return;
|
|
327
914
|
}
|
|
328
915
|
if (command === "run") {
|
|
329
|
-
const inputPath = resolve(
|
|
916
|
+
const inputPath = resolve(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
330
917
|
const configInput = await configFromArgv(argv);
|
|
331
918
|
const config = localRunnerConfigSchema.parse({
|
|
332
919
|
...configInput,
|
|
@@ -347,6 +934,28 @@ async function main(argv) {
|
|
|
347
934
|
? fineTuneRunRequestSchema.parse({ ...input.request, run_id: runId })
|
|
348
935
|
: input.request;
|
|
349
936
|
const request = withParentModelArtifact(baseRequest, await parentModelSelectionFromArgv(argv, config));
|
|
937
|
+
assertLocalRunInputReady(request);
|
|
938
|
+
assertSupportedValidateShape(request, config);
|
|
939
|
+
assertEvaluationScoringReady(config);
|
|
940
|
+
if (hasFlag(argv, "--detach")) {
|
|
941
|
+
const detached = await launchDetachedRun({ argv, request, config });
|
|
942
|
+
const configPath = readOption(argv, "--config");
|
|
943
|
+
const configSuffix = configPath ? ` --config ${JSON.stringify(resolve(configPath))}` : "";
|
|
944
|
+
printJson({
|
|
945
|
+
status: "queued",
|
|
946
|
+
detached: true,
|
|
947
|
+
run_id: request.run_id,
|
|
948
|
+
behavior_spec_id: request.behavior_spec_id,
|
|
949
|
+
pid: detached.pid,
|
|
950
|
+
log_path: detached.logPath,
|
|
951
|
+
artifact_dir: detached.artifactDir,
|
|
952
|
+
next: {
|
|
953
|
+
watch: `tt-local runs watch ${request.run_id}${configSuffix}`,
|
|
954
|
+
cancel: `tt-local runs cancel ${request.run_id}${configSuffix}`,
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
350
959
|
const stage = readRunStage(argv);
|
|
351
960
|
const result = await runLocalFineTuneStage({
|
|
352
961
|
request,
|
|
@@ -379,7 +988,7 @@ async function main(argv) {
|
|
|
379
988
|
}
|
|
380
989
|
else {
|
|
381
990
|
printJson({
|
|
382
|
-
status: "
|
|
991
|
+
status: "stage_completed",
|
|
383
992
|
stage: result.stage,
|
|
384
993
|
input_kind: input.kind,
|
|
385
994
|
run_id: result.request.run_id,
|
|
@@ -392,7 +1001,7 @@ async function main(argv) {
|
|
|
392
1001
|
return;
|
|
393
1002
|
}
|
|
394
1003
|
if (command === "label") {
|
|
395
|
-
const sourcePath =
|
|
1004
|
+
const sourcePath = cli.positionals[0];
|
|
396
1005
|
if (!sourcePath)
|
|
397
1006
|
throw new Error("label requires <input.jsonl|input.csv>");
|
|
398
1007
|
const configInput = await configFromArgv(argv);
|
|
@@ -450,32 +1059,32 @@ async function main(argv) {
|
|
|
450
1059
|
return;
|
|
451
1060
|
}
|
|
452
1061
|
if (command === "runs") {
|
|
453
|
-
const subcommand =
|
|
1062
|
+
const subcommand = cli.subcommand;
|
|
454
1063
|
const config = await configFromArgv(argv);
|
|
455
1064
|
const store = createLocalStore(config.storeRoot);
|
|
456
1065
|
if (subcommand === "list")
|
|
457
1066
|
return printJson(await store.listRuns());
|
|
458
1067
|
if (subcommand === "get") {
|
|
459
|
-
const id =
|
|
1068
|
+
const id = cli.positionals[0];
|
|
460
1069
|
if (!id)
|
|
461
1070
|
throw new Error("runs get requires <run-id>");
|
|
462
1071
|
return printJson(await store.getRun(id));
|
|
463
1072
|
}
|
|
464
1073
|
if (subcommand === "events") {
|
|
465
|
-
const id =
|
|
1074
|
+
const id = cli.positionals[0];
|
|
466
1075
|
if (!id)
|
|
467
1076
|
throw new Error("runs events requires <run-id>");
|
|
468
1077
|
return printJson(await store.getRunEvents(id));
|
|
469
1078
|
}
|
|
470
1079
|
if (subcommand === "report") {
|
|
471
|
-
const id =
|
|
1080
|
+
const id = cli.positionals[0];
|
|
472
1081
|
if (!id)
|
|
473
1082
|
throw new Error("runs report requires <run-id>");
|
|
474
1083
|
return printJson(await store.getRunReport(id));
|
|
475
1084
|
}
|
|
476
1085
|
if (subcommand === "compare") {
|
|
477
|
-
const idA =
|
|
478
|
-
const idB =
|
|
1086
|
+
const idA = cli.positionals[0];
|
|
1087
|
+
const idB = cli.positionals[1];
|
|
479
1088
|
if (!idA || !idB)
|
|
480
1089
|
throw new Error("runs compare requires <run-id-a> <run-id-b>");
|
|
481
1090
|
const [reportA, reportB] = await Promise.all([
|
|
@@ -485,14 +1094,15 @@ async function main(argv) {
|
|
|
485
1094
|
return printJson(compareRuns(reportA, reportB));
|
|
486
1095
|
}
|
|
487
1096
|
if (subcommand === "cancel") {
|
|
488
|
-
const id =
|
|
1097
|
+
const id = cli.positionals[0];
|
|
489
1098
|
if (!id)
|
|
490
1099
|
throw new Error("runs cancel requires <run-id>");
|
|
491
1100
|
await store.cancelRun(id);
|
|
492
|
-
|
|
1101
|
+
const run = await store.getRun(id);
|
|
1102
|
+
return printJson({ ok: true, run_id: run.id, status: run.status, current_stage: run.current_stage });
|
|
493
1103
|
}
|
|
494
1104
|
if (subcommand === "watch") {
|
|
495
|
-
const id =
|
|
1105
|
+
const id = cli.positionals[0];
|
|
496
1106
|
if (!id)
|
|
497
1107
|
throw new Error("runs watch requires <run-id>");
|
|
498
1108
|
const printed = new Set();
|
|
@@ -505,7 +1115,7 @@ async function main(argv) {
|
|
|
505
1115
|
console.log(`${event.occurred_at} ${event.stage} ${event.status}: ${event.message}`);
|
|
506
1116
|
}
|
|
507
1117
|
const run = await store.getRun(id);
|
|
508
|
-
if (
|
|
1118
|
+
if (isTerminalRunState(run))
|
|
509
1119
|
return;
|
|
510
1120
|
await sleep(1000);
|
|
511
1121
|
}
|
|
@@ -517,33 +1127,116 @@ async function main(argv) {
|
|
|
517
1127
|
throw new Error(`Unknown runs command: ${subcommand}`);
|
|
518
1128
|
}
|
|
519
1129
|
if (command === "models") {
|
|
520
|
-
const subcommand =
|
|
1130
|
+
const subcommand = cli.subcommand;
|
|
521
1131
|
const config = await configFromArgv(argv);
|
|
522
1132
|
const store = createLocalStore(config.storeRoot);
|
|
523
1133
|
if (subcommand === "list")
|
|
524
1134
|
return printJson(await store.listModels());
|
|
525
1135
|
if (subcommand === "get") {
|
|
526
|
-
const id =
|
|
1136
|
+
const id = cli.positionals[0];
|
|
527
1137
|
if (!id)
|
|
528
1138
|
throw new Error("models get requires <model-id>");
|
|
529
1139
|
return printJson(await store.getModel(id));
|
|
530
1140
|
}
|
|
1141
|
+
if (subcommand === "verify") {
|
|
1142
|
+
const id = cli.positionals[0];
|
|
1143
|
+
if (!id)
|
|
1144
|
+
throw new Error("models verify requires <model-id-or-artifact-path>");
|
|
1145
|
+
if (await stat(resolve(id)).then(() => true, () => false)) {
|
|
1146
|
+
return printJson({ ok: true, model: null, ...await verifyModelArtifactPath(id) });
|
|
1147
|
+
}
|
|
1148
|
+
const model = await store.getModel(id);
|
|
1149
|
+
const verified = await verifyStoredModel(model);
|
|
1150
|
+
return printJson({ ok: true, model, ...verified });
|
|
1151
|
+
}
|
|
1152
|
+
if (subcommand === "prefetch" || subcommand === "verify-base") {
|
|
1153
|
+
const inputPath = resolve(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
1154
|
+
const input = await loadLocalRunInput(inputPath, {
|
|
1155
|
+
userId: readOption(argv, "--user-id"),
|
|
1156
|
+
runNumber: readNumberOption(argv, "--run-number"),
|
|
1157
|
+
});
|
|
1158
|
+
if (!hasFlag(argv, "--quiet")) {
|
|
1159
|
+
for (const warning of input.warnings) {
|
|
1160
|
+
process.stderr.write(`[tt-local] warning: ${warning}\n`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const report = await prefetchBaseModel({
|
|
1164
|
+
request: input.request,
|
|
1165
|
+
config,
|
|
1166
|
+
localOnly: subcommand === "verify-base",
|
|
1167
|
+
reporter: createConsoleReporter({
|
|
1168
|
+
verbose: hasFlag(argv, "--verbose"),
|
|
1169
|
+
quiet: hasFlag(argv, "--quiet"),
|
|
1170
|
+
}),
|
|
1171
|
+
});
|
|
1172
|
+
return printJson({
|
|
1173
|
+
...report,
|
|
1174
|
+
input_kind: input.kind,
|
|
1175
|
+
input_path: input.path,
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
if (subcommand === "serve") {
|
|
1179
|
+
const id = cli.positionals[0];
|
|
1180
|
+
if (!id)
|
|
1181
|
+
throw new Error("models serve requires <model-id>");
|
|
1182
|
+
const model = await store.getModel(id);
|
|
1183
|
+
const verified = await verifyStoredModel(model, { requireServable: true });
|
|
1184
|
+
const launch = buildLocalModelServerLaunch({
|
|
1185
|
+
model,
|
|
1186
|
+
config,
|
|
1187
|
+
options: {
|
|
1188
|
+
host: readOption(argv, "--host"),
|
|
1189
|
+
port: readNumberOption(argv, "--port"),
|
|
1190
|
+
device: modelServeDevice(argv),
|
|
1191
|
+
maxTokens: readNumberOption(argv, "--max-tokens"),
|
|
1192
|
+
temperature: readNumberOption(argv, "--temperature"),
|
|
1193
|
+
topP: readNumberOption(argv, "--top-p"),
|
|
1194
|
+
maxConcurrentRequests: readNumberOption(argv, "--max-concurrent-requests"),
|
|
1195
|
+
systemPrompt: await modelSystemPrompt({ argv, model, store }),
|
|
1196
|
+
allowRemote: hasFlag(argv, "--allow-remote"),
|
|
1197
|
+
apiKeyEnv: readOption(argv, "--api-key-env"),
|
|
1198
|
+
baseModelRevision: (() => {
|
|
1199
|
+
const contract = verified.contract;
|
|
1200
|
+
return typeof contract.base_model_revision === "string" ? contract.base_model_revision : undefined;
|
|
1201
|
+
})(),
|
|
1202
|
+
baseModelArtifactUri: (() => {
|
|
1203
|
+
const contract = verified.contract;
|
|
1204
|
+
return typeof contract.base_model_artifact_uri === "string" ? contract.base_model_artifact_uri : undefined;
|
|
1205
|
+
})(),
|
|
1206
|
+
},
|
|
1207
|
+
});
|
|
1208
|
+
if (hasFlag(argv, "--print-command")) {
|
|
1209
|
+
return printJson({
|
|
1210
|
+
ok: true,
|
|
1211
|
+
model_id: model.id,
|
|
1212
|
+
url: launch.url,
|
|
1213
|
+
command: launch.displayCommand,
|
|
1214
|
+
artifact_path: launch.artifactPath,
|
|
1215
|
+
manifest_path: verified.manifest_path,
|
|
1216
|
+
integrity: verified.integrity,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
process.stderr.write(`[tt-local] verified ${verified.integrity.checked} artifact file(s)\n`);
|
|
1220
|
+
process.stderr.write(`[tt-local] model API: ${launch.url}\n`);
|
|
1221
|
+
await serveLocalModel(launch);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
531
1224
|
throw new Error(`Unknown models command: ${subcommand}`);
|
|
532
1225
|
}
|
|
533
1226
|
if (command === "specs") {
|
|
534
|
-
const subcommand =
|
|
1227
|
+
const subcommand = cli.subcommand;
|
|
535
1228
|
const config = await configFromArgv(argv);
|
|
536
1229
|
const store = createLocalStore(config.storeRoot);
|
|
537
1230
|
if (subcommand === "list")
|
|
538
1231
|
return printJson(await store.listSpecs());
|
|
539
1232
|
if (subcommand === "get") {
|
|
540
|
-
const id =
|
|
1233
|
+
const id = cli.positionals[0];
|
|
541
1234
|
if (!id)
|
|
542
1235
|
throw new Error("specs get requires <spec-id>");
|
|
543
1236
|
return printJson(await store.getSpec(id));
|
|
544
1237
|
}
|
|
545
1238
|
if (subcommand === "import") {
|
|
546
|
-
const path =
|
|
1239
|
+
const path = cli.positionals[0];
|
|
547
1240
|
if (!path)
|
|
548
1241
|
throw new Error("specs import requires <spec-or-request.json>");
|
|
549
1242
|
const input = JSON.parse(await readFile(resolve(path), "utf8"));
|
|
@@ -566,7 +1259,7 @@ async function main(argv) {
|
|
|
566
1259
|
throw new Error(`Unknown specs command: ${subcommand}`);
|
|
567
1260
|
}
|
|
568
1261
|
if (command === "store") {
|
|
569
|
-
const subcommand =
|
|
1262
|
+
const subcommand = cli.subcommand;
|
|
570
1263
|
const config = await configFromArgv(argv);
|
|
571
1264
|
const store = createLocalStore(config.storeRoot);
|
|
572
1265
|
if (subcommand === "rebuild-index") {
|