@tuned-tensor/local 0.2.7 → 0.2.9
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 +59 -0
- package/README.md +147 -2
- 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 +3 -0
- package/dist/index.js +742 -76
- 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 +13 -0
- package/dist/prefetch.js +107 -19
- package/dist/prefetch.js.map +1 -1
- 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/package.json +4 -2
- package/training/local-runner/src/evaluate.py +80 -20
- package/training/local-runner/src/prefetch.py +107 -8
- 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,26 +1,32 @@
|
|
|
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";
|
|
17
|
+
import { assertUsableModelArtifact, resolveTrainingModel } from "./model-registry.js";
|
|
18
|
+
import { buildLocalModelServerLaunch, serveLocalModel } from "./model-server.js";
|
|
14
19
|
import { prefetchBaseModel } from "./prefetch.js";
|
|
15
|
-
import { createLocalStore } from "./store.js";
|
|
20
|
+
import { createLocalStore, isTerminalRunState } from "./store.js";
|
|
16
21
|
import { serveLocalDashboard } from "./server.js";
|
|
17
|
-
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";
|
|
18
23
|
import { sanitizeLogLine } from "./run-reporter.js";
|
|
19
24
|
export * from "./compare.js";
|
|
20
25
|
export * from "./contracts.js";
|
|
21
26
|
export * from "./dataset.js";
|
|
22
27
|
export * from "./labeling.js";
|
|
23
28
|
export * from "./labeling-sanitize.js";
|
|
29
|
+
export * from "./model-server.js";
|
|
24
30
|
export * from "./orchestrator.js";
|
|
25
31
|
export * from "./local-project.js";
|
|
26
32
|
export * from "./openrouter.js";
|
|
@@ -28,11 +34,22 @@ export * from "./prefetch.js";
|
|
|
28
34
|
export * from "./run-reporter.js";
|
|
29
35
|
export * from "./server.js";
|
|
30
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();
|
|
31
47
|
export function getLocalRunnerInfo() {
|
|
32
48
|
return {
|
|
33
49
|
name: "tuned-tensor-local",
|
|
34
50
|
status: "local-runner-preview",
|
|
35
|
-
description: "Local fine-tuning
|
|
51
|
+
description: "Local-first fine-tuning with baseline-vs-tuned evaluation for small open-weight models.",
|
|
52
|
+
version: TT_LOCAL_VERSION,
|
|
36
53
|
};
|
|
37
54
|
}
|
|
38
55
|
/**
|
|
@@ -81,72 +98,343 @@ async function loadDotEnvFromCwd() {
|
|
|
81
98
|
}
|
|
82
99
|
function readOption(argv, name) {
|
|
83
100
|
const index = argv.indexOf(name);
|
|
84
|
-
if (index
|
|
85
|
-
return
|
|
86
|
-
|
|
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);
|
|
87
105
|
}
|
|
88
106
|
function hasFlag(argv, name) {
|
|
89
107
|
return argv.includes(name);
|
|
90
108
|
}
|
|
91
109
|
function printHelp() {
|
|
92
|
-
console.log(`tt-local
|
|
110
|
+
console.log(`Usage: tt-local <command> [options]
|
|
93
111
|
|
|
94
112
|
Commands:
|
|
95
|
-
info
|
|
96
|
-
init [--name "My Local Model"] [--model Qwen/Qwen3.5-2B] [--output tunedtensor.json] [--force]
|
|
97
|
-
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]
|
|
98
116
|
validate [tunedtensor.json|request.json] [--config local-runner.json]
|
|
99
117
|
run [tunedtensor.json|request.json] [--config local-runner.json] [--stage all|prepare|baseline|train|candidate|score|report]
|
|
100
118
|
[--run-id uuid] [--parent-model local-model-id] [--parent-model-artifact path-or-file-uri]
|
|
101
|
-
[--model-artifact path-or-file-uri] [--force] [--dry-run] [--verbose] [--quiet]
|
|
119
|
+
[--model-artifact path-or-file-uri] [--force] [--dry-run] [--detach] [--verbose] [--quiet]
|
|
102
120
|
label <input.jsonl|input.csv> [--input-column col] [--spec tunedtensor.json] [--system-prompt "..."]
|
|
103
121
|
[--model teacher-model] [--output labeled.jsonl] [--config local-runner.json] [--dry-run]
|
|
104
122
|
serve [--config local-runner.json] [--host 127.0.0.1] [--port 8787]
|
|
105
123
|
runs list|get|events|watch|report|compare|cancel|reconcile [args] [--config local-runner.json]
|
|
106
|
-
models list|get|prefetch [args] [--config local-runner.json]
|
|
124
|
+
models list|get|verify|prefetch|verify-base|serve [args] [--config local-runner.json]
|
|
107
125
|
specs list|get|import [args] [--config local-runner.json]
|
|
108
126
|
store rebuild-index [--config local-runner.json]
|
|
109
127
|
|
|
128
|
+
Global options:
|
|
129
|
+
-h, --help Show help
|
|
130
|
+
-V, --version Show the installed version
|
|
131
|
+
|
|
110
132
|
The run command writes local artifacts under config.artifactRoot, defaulting to
|
|
111
133
|
.tt-local/artifacts. The file-backed local store defaults to
|
|
112
134
|
~/.tuned-tensor-local unless config.storeRoot or TT_LOCAL_HOME is set.`);
|
|
113
135
|
}
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 baseline, fine-tuning, tuned evaluation, and report 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: "Show the baseline-vs-tuned report, including deltas and regressions.", 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();
|
|
135
335
|
const positionals = [];
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
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;
|
|
142
341
|
continue;
|
|
143
342
|
}
|
|
144
|
-
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
|
+
}
|
|
145
368
|
continue;
|
|
146
|
-
|
|
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}`);
|
|
147
377
|
}
|
|
148
378
|
return positionals;
|
|
149
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
|
+
}
|
|
150
438
|
function readNumberOption(argv, name) {
|
|
151
439
|
const value = readOption(argv, name);
|
|
152
440
|
return value ? Number(value) : undefined;
|
|
@@ -235,10 +523,45 @@ async function parentModelSelectionFromArgv(argv, config) {
|
|
|
235
523
|
return undefined;
|
|
236
524
|
const store = createLocalStore(config.storeRoot);
|
|
237
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
|
+
}
|
|
238
558
|
return {
|
|
239
559
|
artifactUri: record.artifact_uri,
|
|
240
560
|
modelId: record.id,
|
|
241
561
|
baseModel: record.base_model,
|
|
562
|
+
baseModelRevision,
|
|
563
|
+
baseModelArtifactUri,
|
|
564
|
+
baseModelFingerprint,
|
|
242
565
|
};
|
|
243
566
|
}
|
|
244
567
|
function withParentModelArtifact(request, parentModel) {
|
|
@@ -248,10 +571,16 @@ function withParentModelArtifact(request, parentModel) {
|
|
|
248
571
|
throw new Error(`Parent model ${parentModel.modelId ?? ""} uses base model ${parentModel.baseModel}, `
|
|
249
572
|
+ `but this run uses ${request.spec_snapshot.base_model}. Continue from a model with the same base model.`);
|
|
250
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
|
+
}
|
|
251
579
|
return fineTuneRunRequestSchema.parse({
|
|
252
580
|
...request,
|
|
253
581
|
hyperparameters: {
|
|
254
582
|
...request.hyperparameters,
|
|
583
|
+
...(parentModel.baseModelRevision ? { base_model_revision: parentModel.baseModelRevision } : {}),
|
|
255
584
|
parent_model_artifact: parentModel.artifactUri,
|
|
256
585
|
},
|
|
257
586
|
});
|
|
@@ -259,26 +588,265 @@ function withParentModelArtifact(request, parentModel) {
|
|
|
259
588
|
async function sleep(ms) {
|
|
260
589
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
261
590
|
}
|
|
262
|
-
function
|
|
263
|
-
|
|
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}`);
|
|
264
813
|
}
|
|
265
814
|
async function main(argv) {
|
|
266
|
-
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
|
+
}
|
|
267
833
|
const loadedEnv = await loadDotEnvFromCwd();
|
|
268
834
|
if (loadedEnv.length > 0 && !hasFlag(argv, "--quiet")) {
|
|
269
835
|
process.stderr.write(`[tt-local] loaded ${loadedEnv.join(", ")} from .env\n`);
|
|
270
836
|
}
|
|
271
|
-
if (command === "info"
|
|
837
|
+
if (command === "info") {
|
|
272
838
|
const info = getLocalRunnerInfo();
|
|
273
839
|
console.log(`${info.name}: ${info.description}`);
|
|
840
|
+
console.log(`Version: ${info.version}`);
|
|
274
841
|
console.log(`Status: ${info.status}`);
|
|
275
|
-
if (command !== "info")
|
|
276
|
-
printHelp();
|
|
277
842
|
return;
|
|
278
843
|
}
|
|
279
844
|
if (command === "doctor") {
|
|
280
845
|
const config = await configFromArgv(argv);
|
|
281
|
-
const
|
|
846
|
+
const request = cli.positionals[0]
|
|
847
|
+
? (await loadLocalRunInput(resolve(cli.positionals[0]))).request
|
|
848
|
+
: undefined;
|
|
849
|
+
const checks = await runDoctor(config, request);
|
|
282
850
|
const ok = checks.every((check) => check.ok);
|
|
283
851
|
printJson({ ok, checks });
|
|
284
852
|
if (!ok)
|
|
@@ -287,35 +855,52 @@ async function main(argv) {
|
|
|
287
855
|
}
|
|
288
856
|
if (command === "init") {
|
|
289
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
|
+
}
|
|
290
862
|
const spec = await initLocalSpecFile({
|
|
291
863
|
outputPath,
|
|
292
864
|
name: readOption(argv, "--name") ?? "Local Tuned Tensor Spec",
|
|
293
865
|
baseModel: readOption(argv, "--model") ?? "Qwen/Qwen3.5-2B",
|
|
294
866
|
force: hasFlag(argv, "--force"),
|
|
295
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
|
+
}
|
|
296
878
|
printJson({
|
|
297
879
|
ok: true,
|
|
298
880
|
path: outputPath,
|
|
299
881
|
id: spec.id,
|
|
300
882
|
name: spec.name,
|
|
301
883
|
base_model: spec.base_model,
|
|
884
|
+
config_path: configPath ?? null,
|
|
302
885
|
});
|
|
303
886
|
return;
|
|
304
887
|
}
|
|
305
888
|
if (command === "validate") {
|
|
306
|
-
const inputPath = resolve(
|
|
889
|
+
const inputPath = resolve(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
307
890
|
const input = await loadLocalRunInput(inputPath, {
|
|
308
891
|
userId: readOption(argv, "--user-id"),
|
|
309
892
|
runNumber: readNumberOption(argv, "--run-number"),
|
|
310
893
|
});
|
|
311
894
|
const request = input.request;
|
|
312
895
|
const config = await configFromArgv(argv);
|
|
896
|
+
assertLocalRunInputReady(request);
|
|
313
897
|
assertSupportedValidateShape(request, config);
|
|
898
|
+
assertEvaluationScoringReady(config);
|
|
314
899
|
printJson({
|
|
315
900
|
ok: true,
|
|
316
901
|
input_kind: input.kind,
|
|
317
902
|
input_path: input.path,
|
|
318
|
-
run_id: request.run_id,
|
|
903
|
+
run_id: input.kind === "request" ? request.run_id : null,
|
|
319
904
|
behavior_spec_id: request.behavior_spec_id,
|
|
320
905
|
training_method: request.training_method,
|
|
321
906
|
base_model: request.spec_snapshot.base_model,
|
|
@@ -328,7 +913,7 @@ async function main(argv) {
|
|
|
328
913
|
return;
|
|
329
914
|
}
|
|
330
915
|
if (command === "run") {
|
|
331
|
-
const inputPath = resolve(
|
|
916
|
+
const inputPath = resolve(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
332
917
|
const configInput = await configFromArgv(argv);
|
|
333
918
|
const config = localRunnerConfigSchema.parse({
|
|
334
919
|
...configInput,
|
|
@@ -349,6 +934,28 @@ async function main(argv) {
|
|
|
349
934
|
? fineTuneRunRequestSchema.parse({ ...input.request, run_id: runId })
|
|
350
935
|
: input.request;
|
|
351
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
|
+
}
|
|
352
959
|
const stage = readRunStage(argv);
|
|
353
960
|
const result = await runLocalFineTuneStage({
|
|
354
961
|
request,
|
|
@@ -381,7 +988,7 @@ async function main(argv) {
|
|
|
381
988
|
}
|
|
382
989
|
else {
|
|
383
990
|
printJson({
|
|
384
|
-
status: "
|
|
991
|
+
status: "stage_completed",
|
|
385
992
|
stage: result.stage,
|
|
386
993
|
input_kind: input.kind,
|
|
387
994
|
run_id: result.request.run_id,
|
|
@@ -394,7 +1001,7 @@ async function main(argv) {
|
|
|
394
1001
|
return;
|
|
395
1002
|
}
|
|
396
1003
|
if (command === "label") {
|
|
397
|
-
const sourcePath =
|
|
1004
|
+
const sourcePath = cli.positionals[0];
|
|
398
1005
|
if (!sourcePath)
|
|
399
1006
|
throw new Error("label requires <input.jsonl|input.csv>");
|
|
400
1007
|
const configInput = await configFromArgv(argv);
|
|
@@ -452,32 +1059,32 @@ async function main(argv) {
|
|
|
452
1059
|
return;
|
|
453
1060
|
}
|
|
454
1061
|
if (command === "runs") {
|
|
455
|
-
const subcommand =
|
|
1062
|
+
const subcommand = cli.subcommand;
|
|
456
1063
|
const config = await configFromArgv(argv);
|
|
457
1064
|
const store = createLocalStore(config.storeRoot);
|
|
458
1065
|
if (subcommand === "list")
|
|
459
1066
|
return printJson(await store.listRuns());
|
|
460
1067
|
if (subcommand === "get") {
|
|
461
|
-
const id =
|
|
1068
|
+
const id = cli.positionals[0];
|
|
462
1069
|
if (!id)
|
|
463
1070
|
throw new Error("runs get requires <run-id>");
|
|
464
1071
|
return printJson(await store.getRun(id));
|
|
465
1072
|
}
|
|
466
1073
|
if (subcommand === "events") {
|
|
467
|
-
const id =
|
|
1074
|
+
const id = cli.positionals[0];
|
|
468
1075
|
if (!id)
|
|
469
1076
|
throw new Error("runs events requires <run-id>");
|
|
470
1077
|
return printJson(await store.getRunEvents(id));
|
|
471
1078
|
}
|
|
472
1079
|
if (subcommand === "report") {
|
|
473
|
-
const id =
|
|
1080
|
+
const id = cli.positionals[0];
|
|
474
1081
|
if (!id)
|
|
475
1082
|
throw new Error("runs report requires <run-id>");
|
|
476
1083
|
return printJson(await store.getRunReport(id));
|
|
477
1084
|
}
|
|
478
1085
|
if (subcommand === "compare") {
|
|
479
|
-
const idA =
|
|
480
|
-
const idB =
|
|
1086
|
+
const idA = cli.positionals[0];
|
|
1087
|
+
const idB = cli.positionals[1];
|
|
481
1088
|
if (!idA || !idB)
|
|
482
1089
|
throw new Error("runs compare requires <run-id-a> <run-id-b>");
|
|
483
1090
|
const [reportA, reportB] = await Promise.all([
|
|
@@ -487,14 +1094,15 @@ async function main(argv) {
|
|
|
487
1094
|
return printJson(compareRuns(reportA, reportB));
|
|
488
1095
|
}
|
|
489
1096
|
if (subcommand === "cancel") {
|
|
490
|
-
const id =
|
|
1097
|
+
const id = cli.positionals[0];
|
|
491
1098
|
if (!id)
|
|
492
1099
|
throw new Error("runs cancel requires <run-id>");
|
|
493
1100
|
await store.cancelRun(id);
|
|
494
|
-
|
|
1101
|
+
const run = await store.getRun(id);
|
|
1102
|
+
return printJson({ ok: true, run_id: run.id, status: run.status, current_stage: run.current_stage });
|
|
495
1103
|
}
|
|
496
1104
|
if (subcommand === "watch") {
|
|
497
|
-
const id =
|
|
1105
|
+
const id = cli.positionals[0];
|
|
498
1106
|
if (!id)
|
|
499
1107
|
throw new Error("runs watch requires <run-id>");
|
|
500
1108
|
const printed = new Set();
|
|
@@ -507,7 +1115,7 @@ async function main(argv) {
|
|
|
507
1115
|
console.log(`${event.occurred_at} ${event.stage} ${event.status}: ${event.message}`);
|
|
508
1116
|
}
|
|
509
1117
|
const run = await store.getRun(id);
|
|
510
|
-
if (
|
|
1118
|
+
if (isTerminalRunState(run))
|
|
511
1119
|
return;
|
|
512
1120
|
await sleep(1000);
|
|
513
1121
|
}
|
|
@@ -519,19 +1127,30 @@ async function main(argv) {
|
|
|
519
1127
|
throw new Error(`Unknown runs command: ${subcommand}`);
|
|
520
1128
|
}
|
|
521
1129
|
if (command === "models") {
|
|
522
|
-
const subcommand =
|
|
1130
|
+
const subcommand = cli.subcommand;
|
|
523
1131
|
const config = await configFromArgv(argv);
|
|
524
1132
|
const store = createLocalStore(config.storeRoot);
|
|
525
1133
|
if (subcommand === "list")
|
|
526
1134
|
return printJson(await store.listModels());
|
|
527
1135
|
if (subcommand === "get") {
|
|
528
|
-
const id =
|
|
1136
|
+
const id = cli.positionals[0];
|
|
529
1137
|
if (!id)
|
|
530
1138
|
throw new Error("models get requires <model-id>");
|
|
531
1139
|
return printJson(await store.getModel(id));
|
|
532
1140
|
}
|
|
533
|
-
if (subcommand === "
|
|
534
|
-
const
|
|
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);
|
|
535
1154
|
const input = await loadLocalRunInput(inputPath, {
|
|
536
1155
|
userId: readOption(argv, "--user-id"),
|
|
537
1156
|
runNumber: readNumberOption(argv, "--run-number"),
|
|
@@ -544,6 +1163,7 @@ async function main(argv) {
|
|
|
544
1163
|
const report = await prefetchBaseModel({
|
|
545
1164
|
request: input.request,
|
|
546
1165
|
config,
|
|
1166
|
+
localOnly: subcommand === "verify-base",
|
|
547
1167
|
reporter: createConsoleReporter({
|
|
548
1168
|
verbose: hasFlag(argv, "--verbose"),
|
|
549
1169
|
quiet: hasFlag(argv, "--quiet"),
|
|
@@ -555,22 +1175,68 @@ async function main(argv) {
|
|
|
555
1175
|
input_path: input.path,
|
|
556
1176
|
});
|
|
557
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
|
+
}
|
|
558
1224
|
throw new Error(`Unknown models command: ${subcommand}`);
|
|
559
1225
|
}
|
|
560
1226
|
if (command === "specs") {
|
|
561
|
-
const subcommand =
|
|
1227
|
+
const subcommand = cli.subcommand;
|
|
562
1228
|
const config = await configFromArgv(argv);
|
|
563
1229
|
const store = createLocalStore(config.storeRoot);
|
|
564
1230
|
if (subcommand === "list")
|
|
565
1231
|
return printJson(await store.listSpecs());
|
|
566
1232
|
if (subcommand === "get") {
|
|
567
|
-
const id =
|
|
1233
|
+
const id = cli.positionals[0];
|
|
568
1234
|
if (!id)
|
|
569
1235
|
throw new Error("specs get requires <spec-id>");
|
|
570
1236
|
return printJson(await store.getSpec(id));
|
|
571
1237
|
}
|
|
572
1238
|
if (subcommand === "import") {
|
|
573
|
-
const path =
|
|
1239
|
+
const path = cli.positionals[0];
|
|
574
1240
|
if (!path)
|
|
575
1241
|
throw new Error("specs import requires <spec-or-request.json>");
|
|
576
1242
|
const input = JSON.parse(await readFile(resolve(path), "utf8"));
|
|
@@ -593,7 +1259,7 @@ async function main(argv) {
|
|
|
593
1259
|
throw new Error(`Unknown specs command: ${subcommand}`);
|
|
594
1260
|
}
|
|
595
1261
|
if (command === "store") {
|
|
596
|
-
const subcommand =
|
|
1262
|
+
const subcommand = cli.subcommand;
|
|
597
1263
|
const config = await configFromArgv(argv);
|
|
598
1264
|
const store = createLocalStore(config.storeRoot);
|
|
599
1265
|
if (subcommand === "rebuild-index") {
|