@tuned-tensor/local 0.2.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +110 -201
- package/dist/active-model.d.ts +16 -0
- package/dist/active-model.js +88 -0
- package/dist/active-model.js.map +1 -0
- package/dist/artifacts.d.ts +5 -4
- package/dist/artifacts.js +57 -33
- package/dist/artifacts.js.map +1 -1
- package/dist/compare.d.ts +1 -8
- package/dist/compare.js +1 -23
- package/dist/compare.js.map +1 -1
- package/dist/contracts.d.ts +234 -365
- package/dist/contracts.js +95 -224
- package/dist/contracts.js.map +1 -1
- package/dist/dataset.d.ts +11 -4
- package/dist/dataset.js +67 -125
- package/dist/dataset.js.map +1 -1
- package/dist/doctor.d.ts +2 -3
- package/dist/doctor.js +23 -161
- package/dist/doctor.js.map +1 -1
- package/dist/evaluation.d.ts +11 -71
- package/dist/evaluation.js +212 -572
- package/dist/evaluation.js.map +1 -1
- package/dist/general-regression.d.ts +10 -0
- package/dist/general-regression.js +16 -0
- package/dist/general-regression.js.map +1 -0
- package/dist/huggingface-cache.d.ts +8 -7
- package/dist/huggingface-cache.js +16 -8
- package/dist/huggingface-cache.js.map +1 -1
- package/dist/index.d.ts +2 -10
- package/dist/index.js +358 -627
- package/dist/index.js.map +1 -1
- package/dist/local-project.d.ts +1 -11
- package/dist/local-project.js +33 -61
- package/dist/local-project.js.map +1 -1
- package/dist/model-registry.d.ts +3 -16
- package/dist/model-registry.js +76 -292
- package/dist/model-registry.js.map +1 -1
- package/dist/model-server.d.ts +7 -3
- package/dist/model-server.js +34 -25
- package/dist/model-server.js.map +1 -1
- package/dist/orchestrator.d.ts +11 -25
- package/dist/orchestrator.js +393 -563
- package/dist/orchestrator.js.map +1 -1
- package/dist/prefetch.d.ts +1 -5
- package/dist/prefetch.js +14 -19
- package/dist/prefetch.js.map +1 -1
- package/dist/process-runner.d.ts +10 -29
- package/dist/process-runner.js +63 -169
- package/dist/process-runner.js.map +1 -1
- package/dist/process-training.d.ts +0 -1
- package/dist/process-training.js +10 -87
- package/dist/process-training.js.map +1 -1
- package/dist/store.d.ts +1 -3
- package/dist/store.js +92 -290
- package/dist/store.js.map +1 -1
- package/docs/architecture.md +87 -152
- package/docs/spark.md +66 -97
- package/examples/dry-runner.json +14 -0
- package/examples/general-regression.jsonl +4 -0
- package/examples/local-runner.json +13 -6
- package/examples/smoke-spec.json +41 -0
- package/package.json +6 -6
- package/training/local-runner/pyproject.toml +0 -5
- package/training/local-runner/src/evaluate.py +89 -234
- package/training/local-runner/src/model_contract.py +47 -0
- package/training/local-runner/src/prefetch.py +18 -4
- package/training/local-runner/src/serve.py +60 -117
- package/training/local-runner/src/sft_data.py +97 -0
- package/training/local-runner/src/train.py +146 -346
- package/training/local-runner/uv.lock +0 -1197
- package/dist/labeling-sanitize.d.ts +0 -31
- package/dist/labeling-sanitize.js +0 -158
- package/dist/labeling-sanitize.js.map +0 -1
- package/dist/labeling.d.ts +0 -155
- package/dist/labeling.js +0 -496
- package/dist/labeling.js.map +0 -1
- package/dist/openrouter.d.ts +0 -29
- package/dist/openrouter.js +0 -66
- package/dist/openrouter.js.map +0 -1
- package/dist/server.d.ts +0 -9
- package/dist/server.js +0 -211
- package/dist/server.js.map +0 -1
- package/docs/local-workflow-remediation-2026-07-13.md +0 -130
- package/docs/local-workflow-ux-review-2026-07-13.md +0 -458
- package/examples/dpo-preferences.jsonl +0 -2
- package/examples/dpo-run-request.json +0 -34
- package/examples/smoke-run-request.json +0 -34
- package/training/local-runner/src/train_dpo.py +0 -286
package/dist/index.js
CHANGED
|
@@ -1,39 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
6
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
7
6
|
import { cwd } from "node:process";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
9
8
|
import { compareRuns } from "./compare.js";
|
|
10
|
-
import { assertArtifactManifest
|
|
11
|
-
import { fineTuneRunRequestSchema, localBehaviorSpecFileSchema, localRunnerConfigSchema
|
|
9
|
+
import { assertArtifactManifest } from "./artifacts.js";
|
|
10
|
+
import { fineTuneRunRequestSchema, localBehaviorSpecFileSchema, localRunnerConfigSchema } from "./contracts.js";
|
|
12
11
|
import { buildSystemMessage } from "./dataset.js";
|
|
13
|
-
import {
|
|
14
|
-
import { runLocalLabelingJob } from "./labeling.js";
|
|
15
|
-
import { loadLocalRunnerConfig, fingerprintLocalBaseModel, runLocalFineTuneStage, } from "./orchestrator.js";
|
|
12
|
+
import { loadLocalRunnerConfig, fingerprintLocalBaseModel, runLocalFineTune, validateLocalFineTuneInput, } from "./orchestrator.js";
|
|
16
13
|
import { runDoctor } from "./doctor.js";
|
|
17
|
-
import { assertUsableModelArtifact
|
|
18
|
-
import { buildLocalModelServerLaunch, serveLocalModel } from "./model-server.js";
|
|
14
|
+
import { assertUsableModelArtifact } from "./model-registry.js";
|
|
15
|
+
import { buildLocalBaseModelServerLaunch, buildLocalModelServerLaunch, serveLocalModel, } from "./model-server.js";
|
|
19
16
|
import { prefetchBaseModel } from "./prefetch.js";
|
|
20
|
-
import { createLocalStore
|
|
21
|
-
import {
|
|
22
|
-
import { DEFAULT_LOCAL_SPEC_PATH, assertLocalRunInputReady, initLocalRunnerConfigFile, initLocalSpecFile, loadLocalRunInput, runRequestFromLocalSpec, } from "./local-project.js";
|
|
17
|
+
import { createLocalStore } from "./store.js";
|
|
18
|
+
import { DEFAULT_LOCAL_SPEC_PATH, assertLocalRunInputReady, initLocalRunnerConfigFile, initLocalSpecFile, loadLocalRunInput, } from "./local-project.js";
|
|
23
19
|
import { sanitizeLogLine } from "./run-reporter.js";
|
|
20
|
+
import { activateModel, getActiveModel, rollbackActiveModel } from "./active-model.js";
|
|
24
21
|
export * from "./compare.js";
|
|
25
22
|
export * from "./contracts.js";
|
|
26
23
|
export * from "./dataset.js";
|
|
27
|
-
export * from "./labeling.js";
|
|
28
|
-
export * from "./labeling-sanitize.js";
|
|
29
24
|
export * from "./model-server.js";
|
|
30
25
|
export * from "./orchestrator.js";
|
|
31
26
|
export * from "./local-project.js";
|
|
32
|
-
export * from "./openrouter.js";
|
|
33
27
|
export * from "./prefetch.js";
|
|
34
28
|
export * from "./run-reporter.js";
|
|
35
|
-
export * from "./server.js";
|
|
36
29
|
export * from "./store.js";
|
|
30
|
+
export * from "./general-regression.js";
|
|
31
|
+
export * from "./active-model.js";
|
|
37
32
|
function packageVersion() {
|
|
38
33
|
try {
|
|
39
34
|
const packageJson = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
|
|
@@ -48,54 +43,10 @@ export function getLocalRunnerInfo() {
|
|
|
48
43
|
return {
|
|
49
44
|
name: "tuned-tensor-local",
|
|
50
45
|
status: "local-runner-preview",
|
|
51
|
-
description: "Local
|
|
46
|
+
description: "Local CUDA LoRA fine-tuning with held-out base-versus-tuned evaluation.",
|
|
52
47
|
version: TT_LOCAL_VERSION,
|
|
53
48
|
};
|
|
54
49
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Parses simple KEY=VALUE lines from .env content. Supports comments, blank
|
|
57
|
-
* lines, an optional `export ` prefix, and single/double quoted values. Does
|
|
58
|
-
* not support multiline values or variable expansion.
|
|
59
|
-
*/
|
|
60
|
-
export function parseDotEnv(content) {
|
|
61
|
-
const values = {};
|
|
62
|
-
for (const rawLine of content.split(/\r?\n/)) {
|
|
63
|
-
const line = rawLine.trim();
|
|
64
|
-
if (!line || line.startsWith("#"))
|
|
65
|
-
continue;
|
|
66
|
-
const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
67
|
-
if (!match)
|
|
68
|
-
continue;
|
|
69
|
-
let value = match[2].trim();
|
|
70
|
-
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
71
|
-
value = value.slice(1, -1);
|
|
72
|
-
}
|
|
73
|
-
values[match[1]] = value;
|
|
74
|
-
}
|
|
75
|
-
return values;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Loads .env from the working directory into process.env without overriding
|
|
79
|
-
* variables that are already set, so `OPENROUTER_API_KEY` and friends work
|
|
80
|
-
* out of the box in project directories. Returns the names that were loaded.
|
|
81
|
-
*/
|
|
82
|
-
async function loadDotEnvFromCwd() {
|
|
83
|
-
let content;
|
|
84
|
-
try {
|
|
85
|
-
content = await readFile(resolve(cwd(), ".env"), "utf8");
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
const loaded = [];
|
|
91
|
-
for (const [key, value] of Object.entries(parseDotEnv(content))) {
|
|
92
|
-
if (process.env[key] === undefined) {
|
|
93
|
-
process.env[key] = value;
|
|
94
|
-
loaded.push(key);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return loaded;
|
|
98
|
-
}
|
|
99
50
|
function readOption(argv, name) {
|
|
100
51
|
const index = argv.indexOf(name);
|
|
101
52
|
if (index !== -1)
|
|
@@ -112,18 +63,12 @@ function printHelp() {
|
|
|
112
63
|
Commands:
|
|
113
64
|
info Show package and runner information
|
|
114
65
|
init [--name "My Local Model"] [--model Qwen/Qwen3.5-2B] [--output tunedtensor.json] [--profile spark] [--force]
|
|
115
|
-
doctor [tunedtensor.json
|
|
116
|
-
validate [tunedtensor.json
|
|
117
|
-
run [tunedtensor.json
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
[--model teacher-model] [--output labeled.jsonl] [--config local-runner.json] [--dry-run]
|
|
122
|
-
serve [--config local-runner.json] [--host 127.0.0.1] [--port 8787]
|
|
123
|
-
runs list|get|events|watch|report|compare|cancel|reconcile [args] [--config local-runner.json]
|
|
124
|
-
models list|get|verify|prefetch|verify-base|serve [args] [--config local-runner.json]
|
|
125
|
-
specs list|get|import [args] [--config local-runner.json]
|
|
126
|
-
store rebuild-index [--config local-runner.json]
|
|
66
|
+
doctor [tunedtensor.json] [--config local-runner.json]
|
|
67
|
+
validate [tunedtensor.json] [--config local-runner.json]
|
|
68
|
+
run [tunedtensor.json] [--config local-runner.json] [--dry-run] [--verbose] [--quiet]
|
|
69
|
+
serve <model-id|active|base> [--config local-runner.json] [--host 127.0.0.1] [--port 8000]
|
|
70
|
+
runs list|get|events|report|compare [args] [--config local-runner.json]
|
|
71
|
+
models list|get|verify|prefetch|verify-base|active|activate|rollback|serve [args] [--config local-runner.json]
|
|
127
72
|
|
|
128
73
|
Global options:
|
|
129
74
|
-h, --help Show help
|
|
@@ -134,10 +79,23 @@ The run command writes local artifacts under config.artifactRoot, defaulting to
|
|
|
134
79
|
~/.tuned-tensor-local unless config.storeRoot or TT_LOCAL_HOME is set.`);
|
|
135
80
|
}
|
|
136
81
|
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
82
|
const VERBOSE_OPTION = { name: "--verbose", description: "Stream subprocess output" };
|
|
140
83
|
const QUIET_OPTION = { name: "--quiet", description: "Suppress progress output on stderr" };
|
|
84
|
+
const MODEL_SERVE_OPTIONS = [
|
|
85
|
+
CONFIG_OPTION,
|
|
86
|
+
{ name: "--host", value: "host", description: "Bind host (localhost by default)" },
|
|
87
|
+
{ name: "--port", value: "port", description: "Bind port" },
|
|
88
|
+
{ name: "--device", value: "device", description: "cuda or cpu" },
|
|
89
|
+
{ name: "--max-tokens", value: "count", description: "Default response token limit" },
|
|
90
|
+
{ name: "--temperature", value: "number", description: "Default sampling temperature" },
|
|
91
|
+
{ name: "--top-p", value: "number", description: "Default nucleus sampling threshold" },
|
|
92
|
+
{ name: "--max-concurrent-requests", value: "count", description: "Concurrent generation limit" },
|
|
93
|
+
{ name: "--spec", value: "path", description: "Behavior spec whose instructions are enforced" },
|
|
94
|
+
{ name: "--no-spec-prompt", description: "Do not enforce the stored behavior-spec prompt" },
|
|
95
|
+
{ name: "--allow-remote", description: "Allow a non-loopback bind" },
|
|
96
|
+
{ name: "--api-key-env", value: "name", description: "Environment variable containing a bearer token" },
|
|
97
|
+
{ name: "--print-command", description: "Validate and print the launch plan without starting" },
|
|
98
|
+
];
|
|
141
99
|
const COMMAND_DEFINITIONS = {
|
|
142
100
|
info: {
|
|
143
101
|
usage: "tt-local info",
|
|
@@ -153,86 +111,53 @@ const COMMAND_DEFINITIONS = {
|
|
|
153
111
|
{ name: "--model", value: "model", description: "Base model ID" },
|
|
154
112
|
{ name: "--output", value: "path", description: "Output spec path" },
|
|
155
113
|
{ name: "--profile", value: "profile", description: "Write a durable runner config (spark)" },
|
|
156
|
-
{ name: "--config
|
|
114
|
+
{ name: "--config", value: "path", description: "Runner config path (written with --profile)" },
|
|
157
115
|
{ name: "--force", description: "Overwrite an existing output file" },
|
|
158
116
|
],
|
|
159
117
|
maxPositionals: 0,
|
|
160
118
|
},
|
|
161
119
|
doctor: {
|
|
162
|
-
usage: "tt-local doctor [tunedtensor.json
|
|
120
|
+
usage: "tt-local doctor [tunedtensor.json] [--config path]",
|
|
163
121
|
description: "Check the host and optional run input before starting work.",
|
|
164
122
|
options: [CONFIG_OPTION],
|
|
165
123
|
maxPositionals: 1,
|
|
166
124
|
},
|
|
167
125
|
validate: {
|
|
168
|
-
usage: "tt-local validate [tunedtensor.json
|
|
169
|
-
description: "Validate a behavior spec
|
|
170
|
-
options: [CONFIG_OPTION
|
|
126
|
+
usage: "tt-local validate [tunedtensor.json] [options]",
|
|
127
|
+
description: "Validate a local behavior spec without executing it.",
|
|
128
|
+
options: [CONFIG_OPTION],
|
|
171
129
|
maxPositionals: 1,
|
|
172
130
|
},
|
|
173
131
|
run: {
|
|
174
|
-
usage: "tt-local run [tunedtensor.json
|
|
132
|
+
usage: "tt-local run [tunedtensor.json] [options]",
|
|
175
133
|
description: "Run the baseline, fine-tuning, tuned evaluation, and report workflow.",
|
|
176
134
|
options: [
|
|
177
135
|
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
136
|
{ 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
137
|
VERBOSE_OPTION,
|
|
189
138
|
QUIET_OPTION,
|
|
190
139
|
],
|
|
191
140
|
maxPositionals: 1,
|
|
192
141
|
},
|
|
193
|
-
|
|
194
|
-
usage: "tt-local
|
|
195
|
-
description: "
|
|
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
|
-
],
|
|
142
|
+
serve: {
|
|
143
|
+
usage: "tt-local serve <model-id|active|base> [options]",
|
|
144
|
+
description: "Serve a verified adapter, the active model, or the protected base.",
|
|
145
|
+
options: MODEL_SERVE_OPTIONS,
|
|
208
146
|
minPositionals: 1,
|
|
209
147
|
maxPositionals: 1,
|
|
210
|
-
missingPositionalsMessage: "
|
|
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,
|
|
148
|
+
missingPositionalsMessage: "serve requires <model-id>",
|
|
221
149
|
},
|
|
222
150
|
};
|
|
223
151
|
const COMMAND_GROUPS = {
|
|
224
152
|
runs: {
|
|
225
|
-
description: "Inspect
|
|
153
|
+
description: "Inspect locally stored runs.",
|
|
226
154
|
defaultSubcommand: "list",
|
|
227
155
|
subcommands: {
|
|
228
156
|
list: { usage: "tt-local runs list [--config path]", description: "List local runs.", options: [CONFIG_OPTION], maxPositionals: 0 },
|
|
229
157
|
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
158
|
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
159
|
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
160
|
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
161
|
},
|
|
237
162
|
},
|
|
238
163
|
models: {
|
|
@@ -250,63 +175,47 @@ const COMMAND_GROUPS = {
|
|
|
250
175
|
missingPositionalsMessage: "models verify requires <model-id-or-artifact-path>",
|
|
251
176
|
},
|
|
252
177
|
prefetch: {
|
|
253
|
-
usage: "tt-local models prefetch [tunedtensor.json
|
|
178
|
+
usage: "tt-local models prefetch [tunedtensor.json] [options]",
|
|
254
179
|
description: "Download the configured base model before a run.",
|
|
255
|
-
options: [CONFIG_OPTION,
|
|
180
|
+
options: [CONFIG_OPTION, VERBOSE_OPTION, QUIET_OPTION],
|
|
256
181
|
maxPositionals: 1,
|
|
257
182
|
},
|
|
258
183
|
"verify-base": {
|
|
259
|
-
usage: "tt-local models verify-base [tunedtensor.json
|
|
184
|
+
usage: "tt-local models verify-base [tunedtensor.json] [options]",
|
|
260
185
|
description: "Verify that the configured base-model snapshot is complete and locally available.",
|
|
261
|
-
options: [CONFIG_OPTION,
|
|
186
|
+
options: [CONFIG_OPTION, VERBOSE_OPTION, QUIET_OPTION],
|
|
262
187
|
maxPositionals: 1,
|
|
263
188
|
},
|
|
264
|
-
|
|
265
|
-
usage: "tt-local models
|
|
266
|
-
description: "
|
|
267
|
-
options: [
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
],
|
|
189
|
+
active: {
|
|
190
|
+
usage: "tt-local models active [--config path]",
|
|
191
|
+
description: "Show the active adapter, or the protected base when none is active.",
|
|
192
|
+
options: [CONFIG_OPTION],
|
|
193
|
+
maxPositionals: 0,
|
|
194
|
+
},
|
|
195
|
+
activate: {
|
|
196
|
+
usage: "tt-local models activate <model-id> [--config path]",
|
|
197
|
+
description: "Activate a verified model whose general regression gate passed.",
|
|
198
|
+
options: [CONFIG_OPTION],
|
|
282
199
|
minPositionals: 1,
|
|
283
200
|
maxPositionals: 1,
|
|
284
|
-
missingPositionalsMessage: "models
|
|
201
|
+
missingPositionalsMessage: "models activate requires <model-id>",
|
|
285
202
|
},
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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" }],
|
|
203
|
+
rollback: {
|
|
204
|
+
usage: "tt-local models rollback [--config path]",
|
|
205
|
+
description: "Restore the previously active adapter or protected base.",
|
|
206
|
+
options: [CONFIG_OPTION],
|
|
207
|
+
maxPositionals: 0,
|
|
208
|
+
},
|
|
209
|
+
serve: {
|
|
210
|
+
usage: "tt-local models serve <model-id|active|base> [options]",
|
|
211
|
+
description: "Alias for `tt-local serve`.",
|
|
212
|
+
options: MODEL_SERVE_OPTIONS,
|
|
298
213
|
minPositionals: 1,
|
|
299
214
|
maxPositionals: 1,
|
|
300
|
-
missingPositionalsMessage: "
|
|
215
|
+
missingPositionalsMessage: "models serve requires <model-id>",
|
|
301
216
|
},
|
|
302
217
|
},
|
|
303
218
|
},
|
|
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
219
|
};
|
|
311
220
|
function hasHelpFlag(argv) {
|
|
312
221
|
return argv.includes("--help") || argv.includes("-h");
|
|
@@ -439,22 +348,32 @@ function readNumberOption(argv, name) {
|
|
|
439
348
|
const value = readOption(argv, name);
|
|
440
349
|
return value ? Number(value) : undefined;
|
|
441
350
|
}
|
|
442
|
-
function
|
|
443
|
-
const
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|| value === "report"
|
|
450
|
-
|| value === "all") {
|
|
451
|
-
return value;
|
|
452
|
-
}
|
|
453
|
-
throw new Error(`--stage must be one of prepare, baseline, train, candidate, score, report, all; got: ${value}`);
|
|
351
|
+
async function selectedConfigPath(argv, adjacentTo) {
|
|
352
|
+
const explicitPath = readOption(argv, "--config");
|
|
353
|
+
if (explicitPath)
|
|
354
|
+
return resolve(explicitPath);
|
|
355
|
+
const candidate = join(adjacentTo ? dirname(resolve(adjacentTo)) : cwd(), "local-runner.json");
|
|
356
|
+
const metadata = await stat(candidate).catch(() => null);
|
|
357
|
+
return metadata?.isFile() ? candidate : undefined;
|
|
454
358
|
}
|
|
455
|
-
async function
|
|
456
|
-
const
|
|
457
|
-
return
|
|
359
|
+
async function configSelectionFromArgv(argv, adjacentTo) {
|
|
360
|
+
const path = await selectedConfigPath(argv, adjacentTo);
|
|
361
|
+
return {
|
|
362
|
+
config: await loadLocalRunnerConfig(path),
|
|
363
|
+
...(path ? { path } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async function configFromArgv(argv, adjacentTo) {
|
|
367
|
+
return (await configSelectionFromArgv(argv, adjacentTo)).config;
|
|
368
|
+
}
|
|
369
|
+
async function loadCliBehaviorSpec(inputPath, runId) {
|
|
370
|
+
const input = await loadLocalRunInput(inputPath, {
|
|
371
|
+
...(runId ? { runId } : {}),
|
|
372
|
+
});
|
|
373
|
+
if (input.kind !== "spec") {
|
|
374
|
+
throw new Error(`TT Local CLI expects a tunedtensor.json behavior spec, not a full run request: ${input.path}`);
|
|
375
|
+
}
|
|
376
|
+
return input;
|
|
458
377
|
}
|
|
459
378
|
function printJson(value) {
|
|
460
379
|
console.log(JSON.stringify(value, null, 2));
|
|
@@ -503,162 +422,7 @@ function createConsoleReporter(options) {
|
|
|
503
422
|
},
|
|
504
423
|
};
|
|
505
424
|
}
|
|
506
|
-
function
|
|
507
|
-
if (request.training_method !== "dpo" || config.training.backend === "command")
|
|
508
|
-
return;
|
|
509
|
-
const model = resolveTrainingModel(request.spec_snapshot.base_model);
|
|
510
|
-
if (model.loader === "image_text_to_text") {
|
|
511
|
-
throw new Error("Bundled DPO training supports text causal-LM models only in v1. Use a causal_lm base model or training.backend=command for a custom DPO trainer.");
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
async function parentModelSelectionFromArgv(argv, config) {
|
|
515
|
-
const parentModel = readOption(argv, "--parent-model");
|
|
516
|
-
const parentModelArtifact = readOption(argv, "--parent-model-artifact");
|
|
517
|
-
if (parentModel && parentModelArtifact) {
|
|
518
|
-
throw new Error("Use only one of --parent-model or --parent-model-artifact.");
|
|
519
|
-
}
|
|
520
|
-
if (parentModelArtifact)
|
|
521
|
-
return { artifactUri: parentModelArtifact };
|
|
522
|
-
if (!parentModel)
|
|
523
|
-
return undefined;
|
|
524
|
-
const store = createLocalStore(config.storeRoot);
|
|
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
|
-
}
|
|
558
|
-
return {
|
|
559
|
-
artifactUri: record.artifact_uri,
|
|
560
|
-
modelId: record.id,
|
|
561
|
-
baseModel: record.base_model,
|
|
562
|
-
baseModelRevision,
|
|
563
|
-
baseModelArtifactUri,
|
|
564
|
-
baseModelFingerprint,
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
function withParentModelArtifact(request, parentModel) {
|
|
568
|
-
if (!parentModel)
|
|
569
|
-
return request;
|
|
570
|
-
if (parentModel.baseModel && parentModel.baseModel !== request.spec_snapshot.base_model) {
|
|
571
|
-
throw new Error(`Parent model ${parentModel.modelId ?? ""} uses base model ${parentModel.baseModel}, `
|
|
572
|
-
+ `but this run uses ${request.spec_snapshot.base_model}. Continue from a model with the same base model.`);
|
|
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
|
-
}
|
|
579
|
-
return fineTuneRunRequestSchema.parse({
|
|
580
|
-
...request,
|
|
581
|
-
hyperparameters: {
|
|
582
|
-
...request.hyperparameters,
|
|
583
|
-
...(parentModel.baseModelRevision ? { base_model_revision: parentModel.baseModelRevision } : {}),
|
|
584
|
-
parent_model_artifact: parentModel.artifactUri,
|
|
585
|
-
},
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
async function sleep(ms) {
|
|
589
|
-
await new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
590
|
-
}
|
|
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 = {}) {
|
|
425
|
+
async function verifyStoredModel(model) {
|
|
662
426
|
const manifestPath = join(model.artifact_dir, "artifact-manifest.json");
|
|
663
427
|
const integrity = await assertArtifactManifest(manifestPath, {
|
|
664
428
|
requiredPaths: ["stage-metadata.json", "training-report.json"],
|
|
@@ -669,11 +433,7 @@ async function verifyStoredModel(model, options = {}) {
|
|
|
669
433
|
if (!manifest.model) {
|
|
670
434
|
throw new Error(`Artifact manifest does not contain a model contract: ${manifestPath}`);
|
|
671
435
|
}
|
|
672
|
-
const
|
|
673
|
-
&& manifest.model.servable === false;
|
|
674
|
-
const artifact = await assertUsableModelArtifact(model.artifact_uri, {
|
|
675
|
-
allowUnrecognizedPayload: customNonServable && !options.requireServable,
|
|
676
|
-
});
|
|
436
|
+
const artifact = await assertUsableModelArtifact(model.artifact_uri);
|
|
677
437
|
if (typeof manifest.model.base_model_artifact_uri === "string"
|
|
678
438
|
&& typeof manifest.model.base_model_fingerprint === "string") {
|
|
679
439
|
const actualBaseFingerprint = await fingerprintLocalBaseModel(manifest.model.base_model_artifact_uri);
|
|
@@ -688,9 +448,6 @@ async function verifyStoredModel(model, options = {}) {
|
|
|
688
448
|
if (manifest.model.base_model !== model.base_model) {
|
|
689
449
|
throw new Error("Stored model base model does not match its artifact manifest.");
|
|
690
450
|
}
|
|
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
451
|
return {
|
|
695
452
|
manifest_path: manifestPath,
|
|
696
453
|
integrity,
|
|
@@ -698,6 +455,17 @@ async function verifyStoredModel(model, options = {}) {
|
|
|
698
455
|
contract: manifest.model,
|
|
699
456
|
};
|
|
700
457
|
}
|
|
458
|
+
async function verifyActivationEvidence(model) {
|
|
459
|
+
await assertArtifactManifest(join(model.artifact_dir, "artifact-manifest.json"), {
|
|
460
|
+
requiredPaths: [
|
|
461
|
+
"run-report.json",
|
|
462
|
+
"general-baseline-eval.json",
|
|
463
|
+
"general-candidate-eval.json",
|
|
464
|
+
],
|
|
465
|
+
scopeToRequired: true,
|
|
466
|
+
verifyModel: true,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
701
469
|
async function verifyModelArtifactPath(input) {
|
|
702
470
|
const inputPath = resolve(input);
|
|
703
471
|
let manifestPath;
|
|
@@ -743,11 +511,7 @@ async function verifyModelArtifactPath(input) {
|
|
|
743
511
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
744
512
|
if (!manifest.model)
|
|
745
513
|
throw new Error(`Artifact manifest does not contain a model contract: ${manifestPath}`);
|
|
746
|
-
const
|
|
747
|
-
&& manifest.model.servable === false;
|
|
748
|
-
const artifact = await assertUsableModelArtifact(artifactUri, {
|
|
749
|
-
allowUnrecognizedPayload: customNonServable,
|
|
750
|
-
});
|
|
514
|
+
const artifact = await assertUsableModelArtifact(artifactUri);
|
|
751
515
|
if (typeof manifest.model.base_model_artifact_uri === "string"
|
|
752
516
|
&& typeof manifest.model.base_model_fingerprint === "string") {
|
|
753
517
|
const actualBaseFingerprint = await fingerprintLocalBaseModel(manifest.model.base_model_artifact_uri);
|
|
@@ -772,12 +536,10 @@ async function modelSystemPrompt(args) {
|
|
|
772
536
|
if (specPath) {
|
|
773
537
|
const input = JSON.parse(await readFile(resolve(specPath), "utf8"));
|
|
774
538
|
const local = localBehaviorSpecFileSchema.safeParse(input);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
throw new Error(`--spec must contain a TT Local behavior spec or run request: ${resolve(specPath)}`);
|
|
780
|
-
spec = parsedSpec;
|
|
539
|
+
if (!local.success) {
|
|
540
|
+
throw new Error(`--spec must contain a tunedtensor.json behavior spec: ${resolve(specPath)}`);
|
|
541
|
+
}
|
|
542
|
+
spec = local.data;
|
|
781
543
|
}
|
|
782
544
|
else {
|
|
783
545
|
const runRequestPath = join(args.store.paths.runsDir, args.model.run_id, "request.json");
|
|
@@ -806,10 +568,140 @@ async function modelSystemPrompt(args) {
|
|
|
806
568
|
}
|
|
807
569
|
function modelServeDevice(argv) {
|
|
808
570
|
const value = readOption(argv, "--device");
|
|
809
|
-
if (value === undefined || value === "
|
|
571
|
+
if (value === undefined || value === "cpu" || value === "cuda") {
|
|
810
572
|
return value;
|
|
811
573
|
}
|
|
812
|
-
throw new Error(`--device must be
|
|
574
|
+
throw new Error(`--device must be cuda or cpu; got: ${value}`);
|
|
575
|
+
}
|
|
576
|
+
async function serveStoredModelFromCli(args) {
|
|
577
|
+
const store = createLocalStore(args.config.storeRoot);
|
|
578
|
+
const model = await store.getModel(args.modelId);
|
|
579
|
+
const verified = await verifyStoredModel(model);
|
|
580
|
+
const launch = buildLocalModelServerLaunch({
|
|
581
|
+
model,
|
|
582
|
+
config: args.config,
|
|
583
|
+
options: {
|
|
584
|
+
host: readOption(args.argv, "--host"),
|
|
585
|
+
port: readNumberOption(args.argv, "--port"),
|
|
586
|
+
device: modelServeDevice(args.argv),
|
|
587
|
+
maxTokens: readNumberOption(args.argv, "--max-tokens"),
|
|
588
|
+
temperature: readNumberOption(args.argv, "--temperature"),
|
|
589
|
+
topP: readNumberOption(args.argv, "--top-p"),
|
|
590
|
+
maxConcurrentRequests: readNumberOption(args.argv, "--max-concurrent-requests"),
|
|
591
|
+
systemPrompt: await modelSystemPrompt({ argv: args.argv, model, store }),
|
|
592
|
+
allowRemote: hasFlag(args.argv, "--allow-remote"),
|
|
593
|
+
apiKeyEnv: readOption(args.argv, "--api-key-env"),
|
|
594
|
+
baseModelRevision: (() => {
|
|
595
|
+
const contract = verified.contract;
|
|
596
|
+
return typeof contract.base_model_revision === "string" ? contract.base_model_revision : undefined;
|
|
597
|
+
})(),
|
|
598
|
+
baseModelArtifactUri: (() => {
|
|
599
|
+
const contract = verified.contract;
|
|
600
|
+
return typeof contract.base_model_artifact_uri === "string" ? contract.base_model_artifact_uri : undefined;
|
|
601
|
+
})(),
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
if (hasFlag(args.argv, "--print-command")) {
|
|
605
|
+
printJson({
|
|
606
|
+
ok: true,
|
|
607
|
+
model_id: model.id,
|
|
608
|
+
url: launch.url,
|
|
609
|
+
command: launch.displayCommand,
|
|
610
|
+
artifact_path: launch.artifactPath,
|
|
611
|
+
manifest_path: verified.manifest_path,
|
|
612
|
+
integrity: verified.integrity,
|
|
613
|
+
});
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
process.stderr.write(`[tt-local] verified ${verified.integrity.checked} artifact file(s)\n`);
|
|
617
|
+
process.stderr.write(`[tt-local] model API: ${launch.url}\n`);
|
|
618
|
+
await serveLocalModel(launch);
|
|
619
|
+
}
|
|
620
|
+
function serveOptionsFromArgv(argv) {
|
|
621
|
+
return {
|
|
622
|
+
host: readOption(argv, "--host"),
|
|
623
|
+
port: readNumberOption(argv, "--port"),
|
|
624
|
+
device: modelServeDevice(argv),
|
|
625
|
+
maxTokens: readNumberOption(argv, "--max-tokens"),
|
|
626
|
+
temperature: readNumberOption(argv, "--temperature"),
|
|
627
|
+
topP: readNumberOption(argv, "--top-p"),
|
|
628
|
+
maxConcurrentRequests: readNumberOption(argv, "--max-concurrent-requests"),
|
|
629
|
+
allowRemote: hasFlag(argv, "--allow-remote"),
|
|
630
|
+
apiKeyEnv: readOption(argv, "--api-key-env"),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
async function serveBaseModelFromCli(args) {
|
|
634
|
+
const explicitSpecPath = readOption(args.argv, "--spec");
|
|
635
|
+
if (explicitSpecPath && hasFlag(args.argv, "--no-spec-prompt")) {
|
|
636
|
+
throw new Error("Use only one of --spec or --no-spec-prompt.");
|
|
637
|
+
}
|
|
638
|
+
const defaultSpecPath = resolve(DEFAULT_LOCAL_SPEC_PATH);
|
|
639
|
+
const specPath = explicitSpecPath
|
|
640
|
+
? resolve(explicitSpecPath)
|
|
641
|
+
: (await stat(defaultSpecPath).catch(() => null))?.isFile()
|
|
642
|
+
? defaultSpecPath
|
|
643
|
+
: undefined;
|
|
644
|
+
const spec = specPath
|
|
645
|
+
? localBehaviorSpecFileSchema.parse(JSON.parse(await readFile(specPath, "utf8")))
|
|
646
|
+
: undefined;
|
|
647
|
+
let baseModel = spec?.base_model;
|
|
648
|
+
if (!baseModel) {
|
|
649
|
+
const store = createLocalStore(args.config.storeRoot);
|
|
650
|
+
const active = await getActiveModel(store);
|
|
651
|
+
const referenceModel = active.model
|
|
652
|
+
?? (active.pointer?.previous_model_id
|
|
653
|
+
? await store.getModel(active.pointer.previous_model_id)
|
|
654
|
+
: null);
|
|
655
|
+
baseModel = referenceModel?.base_model;
|
|
656
|
+
}
|
|
657
|
+
if (!baseModel) {
|
|
658
|
+
throw new Error("Cannot identify the protected base model. Pass --spec <tunedtensor.json> "
|
|
659
|
+
+ "or activate a model first.");
|
|
660
|
+
}
|
|
661
|
+
const launch = buildLocalBaseModelServerLaunch({
|
|
662
|
+
baseModel,
|
|
663
|
+
config: args.config,
|
|
664
|
+
options: {
|
|
665
|
+
...serveOptionsFromArgv(args.argv),
|
|
666
|
+
systemPrompt: explicitSpecPath && !hasFlag(args.argv, "--no-spec-prompt")
|
|
667
|
+
? buildSystemMessage(spec)
|
|
668
|
+
: undefined,
|
|
669
|
+
baseModelRevision: spec?.hyperparameters?.base_model_revision,
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
if (hasFlag(args.argv, "--print-command")) {
|
|
673
|
+
printJson({
|
|
674
|
+
ok: true,
|
|
675
|
+
model_id: null,
|
|
676
|
+
base_model: baseModel,
|
|
677
|
+
url: launch.url,
|
|
678
|
+
command: launch.displayCommand,
|
|
679
|
+
});
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
process.stderr.write(`[tt-local] protected base model API: ${launch.url}\n`);
|
|
683
|
+
await serveLocalModel(launch);
|
|
684
|
+
}
|
|
685
|
+
async function serveModelTargetFromCli(args) {
|
|
686
|
+
if (args.target === "base") {
|
|
687
|
+
return serveBaseModelFromCli(args);
|
|
688
|
+
}
|
|
689
|
+
if (args.target === "active") {
|
|
690
|
+
const store = createLocalStore(args.config.storeRoot);
|
|
691
|
+
const active = await getActiveModel(store);
|
|
692
|
+
if (!active.model)
|
|
693
|
+
return serveBaseModelFromCli(args);
|
|
694
|
+
return serveStoredModelFromCli({
|
|
695
|
+
argv: args.argv,
|
|
696
|
+
modelId: active.model.id,
|
|
697
|
+
config: args.config,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return serveStoredModelFromCli({
|
|
701
|
+
argv: args.argv,
|
|
702
|
+
modelId: args.target,
|
|
703
|
+
config: args.config,
|
|
704
|
+
});
|
|
813
705
|
}
|
|
814
706
|
async function main(argv) {
|
|
815
707
|
const cli = parseCli(argv);
|
|
@@ -830,10 +722,6 @@ async function main(argv) {
|
|
|
830
722
|
printCommandHelp(cli.definition);
|
|
831
723
|
return;
|
|
832
724
|
}
|
|
833
|
-
const loadedEnv = await loadDotEnvFromCwd();
|
|
834
|
-
if (loadedEnv.length > 0 && !hasFlag(argv, "--quiet")) {
|
|
835
|
-
process.stderr.write(`[tt-local] loaded ${loadedEnv.join(", ")} from .env\n`);
|
|
836
|
-
}
|
|
837
725
|
if (command === "info") {
|
|
838
726
|
const info = getLocalRunnerInfo();
|
|
839
727
|
console.log(`${info.name}: ${info.description}`);
|
|
@@ -842,13 +730,21 @@ async function main(argv) {
|
|
|
842
730
|
return;
|
|
843
731
|
}
|
|
844
732
|
if (command === "doctor") {
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
|
|
733
|
+
const inputPath = resolve(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
734
|
+
const configSelection = await configSelectionFromArgv(argv, inputPath);
|
|
735
|
+
const hasDefaultSpec = cli.positionals[0]
|
|
736
|
+
? true
|
|
737
|
+
: Boolean((await stat(inputPath).catch(() => null))?.isFile());
|
|
738
|
+
const request = hasDefaultSpec
|
|
739
|
+
? (await loadCliBehaviorSpec(inputPath)).request
|
|
848
740
|
: undefined;
|
|
849
|
-
const checks = await runDoctor(config, request);
|
|
741
|
+
const checks = await runDoctor(configSelection.config, request);
|
|
850
742
|
const ok = checks.every((check) => check.ok);
|
|
851
|
-
printJson({
|
|
743
|
+
printJson({
|
|
744
|
+
ok,
|
|
745
|
+
config_path: configSelection.path ?? null,
|
|
746
|
+
checks,
|
|
747
|
+
});
|
|
852
748
|
if (!ok)
|
|
853
749
|
process.exitCode = 1;
|
|
854
750
|
return;
|
|
@@ -866,15 +762,18 @@ async function main(argv) {
|
|
|
866
762
|
force: hasFlag(argv, "--force"),
|
|
867
763
|
});
|
|
868
764
|
const configPath = profile
|
|
869
|
-
? resolve(readOption(argv, "--config
|
|
870
|
-
:
|
|
871
|
-
if (profile
|
|
765
|
+
? resolve(readOption(argv, "--config") ?? resolve(dirname(outputPath), "local-runner.json"))
|
|
766
|
+
: await selectedConfigPath(argv, outputPath);
|
|
767
|
+
if (profile) {
|
|
872
768
|
await initLocalRunnerConfigFile({
|
|
873
769
|
outputPath: configPath,
|
|
874
770
|
profile,
|
|
875
771
|
force: hasFlag(argv, "--force"),
|
|
876
772
|
});
|
|
877
773
|
}
|
|
774
|
+
else if (configPath) {
|
|
775
|
+
await loadLocalRunnerConfig(configPath);
|
|
776
|
+
}
|
|
878
777
|
printJson({
|
|
879
778
|
ok: true,
|
|
880
779
|
path: outputPath,
|
|
@@ -887,174 +786,95 @@ async function main(argv) {
|
|
|
887
786
|
}
|
|
888
787
|
if (command === "validate") {
|
|
889
788
|
const inputPath = resolve(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
890
|
-
const input = await
|
|
891
|
-
|
|
892
|
-
|
|
789
|
+
const input = await loadCliBehaviorSpec(inputPath);
|
|
790
|
+
assertLocalRunInputReady(input.request);
|
|
791
|
+
const configSelection = await configSelectionFromArgv(argv, inputPath);
|
|
792
|
+
const validated = await validateLocalFineTuneInput({
|
|
793
|
+
request: input.request,
|
|
794
|
+
config: configSelection.config,
|
|
893
795
|
});
|
|
894
|
-
const request =
|
|
895
|
-
const config =
|
|
896
|
-
assertLocalRunInputReady(request);
|
|
897
|
-
assertSupportedValidateShape(request, config);
|
|
898
|
-
assertEvaluationScoringReady(config);
|
|
796
|
+
const request = validated.request;
|
|
797
|
+
const config = validated.config;
|
|
899
798
|
printJson({
|
|
900
799
|
ok: true,
|
|
901
|
-
input_kind: input.kind,
|
|
902
800
|
input_path: input.path,
|
|
903
|
-
|
|
801
|
+
config_path: configSelection.path ?? null,
|
|
904
802
|
behavior_spec_id: request.behavior_spec_id,
|
|
905
|
-
training_method: request.training_method,
|
|
906
803
|
base_model: request.spec_snapshot.base_model,
|
|
907
804
|
dataset_format: request.dataset_prebuilt?.format ?? null,
|
|
908
805
|
artifact_root: config.artifactRoot,
|
|
909
806
|
store_root: config.storeRoot,
|
|
910
807
|
dry_run: config.dryRun,
|
|
911
|
-
...(input.warnings.length > 0 ? { warnings: input.warnings } : {}),
|
|
912
808
|
});
|
|
913
809
|
return;
|
|
914
810
|
}
|
|
915
811
|
if (command === "run") {
|
|
916
812
|
const inputPath = resolve(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
917
|
-
const
|
|
813
|
+
const configSelection = await configSelectionFromArgv(argv, inputPath);
|
|
814
|
+
const configInput = configSelection.config;
|
|
918
815
|
const config = localRunnerConfigSchema.parse({
|
|
919
816
|
...configInput,
|
|
920
817
|
dryRun: hasFlag(argv, "--dry-run") ? true : configInput.dryRun,
|
|
921
818
|
});
|
|
922
|
-
const input = await
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
819
|
+
const input = await loadCliBehaviorSpec(inputPath);
|
|
820
|
+
assertLocalRunInputReady(input.request);
|
|
821
|
+
const validated = await validateLocalFineTuneInput({
|
|
822
|
+
request: input.request,
|
|
823
|
+
config,
|
|
824
|
+
});
|
|
825
|
+
let request = validated.request;
|
|
826
|
+
const reporter = createConsoleReporter({
|
|
827
|
+
verbose: hasFlag(argv, "--verbose"),
|
|
828
|
+
quiet: hasFlag(argv, "--quiet"),
|
|
926
829
|
});
|
|
927
|
-
if (!
|
|
928
|
-
|
|
929
|
-
|
|
830
|
+
if (!config.dryRun
|
|
831
|
+
&& !config.paths.baseModel
|
|
832
|
+
&& !request.hyperparameters.base_model_revision) {
|
|
833
|
+
const prefetch = await prefetchBaseModel({
|
|
834
|
+
request,
|
|
835
|
+
config,
|
|
836
|
+
reporter,
|
|
837
|
+
});
|
|
838
|
+
if (!prefetch.snapshot_revision) {
|
|
839
|
+
throw new Error("Base-model prefetch did not return an immutable snapshot revision.");
|
|
930
840
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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}`,
|
|
841
|
+
request = fineTuneRunRequestSchema.parse({
|
|
842
|
+
...request,
|
|
843
|
+
hyperparameters: {
|
|
844
|
+
...request.hyperparameters,
|
|
845
|
+
base_model_revision: prefetch.snapshot_revision,
|
|
955
846
|
},
|
|
956
847
|
});
|
|
957
|
-
return;
|
|
958
848
|
}
|
|
959
|
-
const
|
|
960
|
-
const result = await runLocalFineTuneStage({
|
|
849
|
+
const result = await runLocalFineTune({
|
|
961
850
|
request,
|
|
962
851
|
config,
|
|
963
|
-
|
|
964
|
-
force: hasFlag(argv, "--force"),
|
|
965
|
-
modelArtifact: readOption(argv, "--model-artifact"),
|
|
966
|
-
reporter: createConsoleReporter({
|
|
967
|
-
verbose: hasFlag(argv, "--verbose"),
|
|
968
|
-
quiet: hasFlag(argv, "--quiet"),
|
|
969
|
-
}),
|
|
852
|
+
reporter,
|
|
970
853
|
});
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
report_path: result.reportPath,
|
|
979
|
-
artifact_dir: result.artifactDir,
|
|
854
|
+
printJson({
|
|
855
|
+
status: result.report.status,
|
|
856
|
+
run_id: result.report.run_id,
|
|
857
|
+
behavior_spec_id: result.report.behavior_spec_id,
|
|
858
|
+
report_path: result.reportPath,
|
|
859
|
+
artifact_dir: result.artifactDir,
|
|
860
|
+
...(!config.dryRun ? {
|
|
980
861
|
model_id: `local-${result.report.run_id}`,
|
|
981
862
|
fine_tuned_model_id: result.report.fine_tuned_model_id,
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
}
|
|
989
|
-
else {
|
|
990
|
-
printJson({
|
|
991
|
-
status: "stage_completed",
|
|
992
|
-
stage: result.stage,
|
|
993
|
-
input_kind: input.kind,
|
|
994
|
-
run_id: result.request.run_id,
|
|
995
|
-
behavior_spec_id: result.request.behavior_spec_id,
|
|
996
|
-
artifact_dir: result.artifactDir,
|
|
997
|
-
parent_model_artifact: result.request.hyperparameters.parent_model_artifact ?? null,
|
|
998
|
-
artifacts: result.artifacts,
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
if (command === "label") {
|
|
1004
|
-
const sourcePath = cli.positionals[0];
|
|
1005
|
-
if (!sourcePath)
|
|
1006
|
-
throw new Error("label requires <input.jsonl|input.csv>");
|
|
1007
|
-
const configInput = await configFromArgv(argv);
|
|
1008
|
-
const config = localRunnerConfigSchema.parse({
|
|
1009
|
-
...configInput,
|
|
1010
|
-
dryRun: hasFlag(argv, "--dry-run") ? true : configInput.dryRun,
|
|
1011
|
-
});
|
|
1012
|
-
const format = readOption(argv, "--format");
|
|
1013
|
-
if (format !== undefined && format !== "jsonl" && format !== "csv") {
|
|
1014
|
-
throw new Error(`--format must be jsonl or csv, got: ${format}`);
|
|
1015
|
-
}
|
|
1016
|
-
let systemMessage = readOption(argv, "--system-prompt");
|
|
1017
|
-
if (!systemMessage) {
|
|
1018
|
-
const specPath = resolve(readOption(argv, "--spec") ?? DEFAULT_LOCAL_SPEC_PATH);
|
|
1019
|
-
let raw;
|
|
1020
|
-
try {
|
|
1021
|
-
raw = await readFile(specPath, "utf8");
|
|
1022
|
-
}
|
|
1023
|
-
catch {
|
|
1024
|
-
throw new Error(`Cannot read spec at ${specPath}. Pass --spec <tunedtensor.json> or --system-prompt "..." so the teacher labels under the model's instructions.`);
|
|
1025
|
-
}
|
|
1026
|
-
systemMessage = buildSystemMessage(localBehaviorSpecFileSchema.parse(JSON.parse(raw)));
|
|
1027
|
-
}
|
|
1028
|
-
const result = await runLocalLabelingJob({
|
|
1029
|
-
sourcePath: resolve(sourcePath),
|
|
1030
|
-
format,
|
|
1031
|
-
inputColumn: readOption(argv, "--input-column"),
|
|
1032
|
-
systemMessage,
|
|
1033
|
-
config,
|
|
1034
|
-
outputPath: readOption(argv, "--output"),
|
|
1035
|
-
model: readOption(argv, "--model"),
|
|
1036
|
-
reporter: createConsoleReporter({
|
|
1037
|
-
verbose: hasFlag(argv, "--verbose"),
|
|
1038
|
-
quiet: hasFlag(argv, "--quiet"),
|
|
1039
|
-
}),
|
|
863
|
+
} : {}),
|
|
864
|
+
training_log: result.report.training.log_uri,
|
|
865
|
+
baseline_eval: result.report.artifact_uris.baseline_eval,
|
|
866
|
+
candidate_eval: result.report.artifact_uris.candidate_eval,
|
|
867
|
+
comparison: result.report.comparison,
|
|
868
|
+
general_regression: result.report.general_regression,
|
|
1040
869
|
});
|
|
1041
|
-
printJson(result.report);
|
|
1042
|
-
if (result.report.status === "failed")
|
|
1043
|
-
process.exitCode = 1;
|
|
1044
870
|
return;
|
|
1045
871
|
}
|
|
1046
872
|
if (command === "serve") {
|
|
1047
873
|
const config = await configFromArgv(argv);
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
await new Promise((resolveStop) => {
|
|
1053
|
-
const stop = () => {
|
|
1054
|
-
dashboard.close().then(resolveStop, resolveStop);
|
|
1055
|
-
};
|
|
1056
|
-
process.once("SIGINT", stop);
|
|
1057
|
-
process.once("SIGTERM", stop);
|
|
874
|
+
await serveModelTargetFromCli({
|
|
875
|
+
argv,
|
|
876
|
+
target: cli.positionals[0],
|
|
877
|
+
config,
|
|
1058
878
|
});
|
|
1059
879
|
return;
|
|
1060
880
|
}
|
|
@@ -1093,42 +913,14 @@ async function main(argv) {
|
|
|
1093
913
|
]);
|
|
1094
914
|
return printJson(compareRuns(reportA, reportB));
|
|
1095
915
|
}
|
|
1096
|
-
if (subcommand === "cancel") {
|
|
1097
|
-
const id = cli.positionals[0];
|
|
1098
|
-
if (!id)
|
|
1099
|
-
throw new Error("runs cancel requires <run-id>");
|
|
1100
|
-
await store.cancelRun(id);
|
|
1101
|
-
const run = await store.getRun(id);
|
|
1102
|
-
return printJson({ ok: true, run_id: run.id, status: run.status, current_stage: run.current_stage });
|
|
1103
|
-
}
|
|
1104
|
-
if (subcommand === "watch") {
|
|
1105
|
-
const id = cli.positionals[0];
|
|
1106
|
-
if (!id)
|
|
1107
|
-
throw new Error("runs watch requires <run-id>");
|
|
1108
|
-
const printed = new Set();
|
|
1109
|
-
while (true) {
|
|
1110
|
-
const events = await store.getRunEvents(id);
|
|
1111
|
-
for (const event of events) {
|
|
1112
|
-
if (printed.has(event.id))
|
|
1113
|
-
continue;
|
|
1114
|
-
printed.add(event.id);
|
|
1115
|
-
console.log(`${event.occurred_at} ${event.stage} ${event.status}: ${event.message}`);
|
|
1116
|
-
}
|
|
1117
|
-
const run = await store.getRun(id);
|
|
1118
|
-
if (isTerminalRunState(run))
|
|
1119
|
-
return;
|
|
1120
|
-
await sleep(1000);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
if (subcommand === "reconcile") {
|
|
1124
|
-
await store.rebuildIndexes();
|
|
1125
|
-
return printJson({ ok: true });
|
|
1126
|
-
}
|
|
1127
916
|
throw new Error(`Unknown runs command: ${subcommand}`);
|
|
1128
917
|
}
|
|
1129
918
|
if (command === "models") {
|
|
1130
919
|
const subcommand = cli.subcommand;
|
|
1131
|
-
const
|
|
920
|
+
const modelInputPath = subcommand === "prefetch" || subcommand === "verify-base"
|
|
921
|
+
? resolve(cli.positionals[0] ?? DEFAULT_LOCAL_SPEC_PATH)
|
|
922
|
+
: undefined;
|
|
923
|
+
const config = await configFromArgv(argv, modelInputPath);
|
|
1132
924
|
const store = createLocalStore(config.storeRoot);
|
|
1133
925
|
if (subcommand === "list")
|
|
1134
926
|
return printJson(await store.listModels());
|
|
@@ -1149,17 +941,41 @@ async function main(argv) {
|
|
|
1149
941
|
const verified = await verifyStoredModel(model);
|
|
1150
942
|
return printJson({ ok: true, model, ...verified });
|
|
1151
943
|
}
|
|
1152
|
-
if (subcommand === "
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
944
|
+
if (subcommand === "active") {
|
|
945
|
+
const active = await getActiveModel(store);
|
|
946
|
+
return printJson({
|
|
947
|
+
active: active.model?.id ?? "base",
|
|
948
|
+
pointer: active.pointer,
|
|
949
|
+
model: active.model,
|
|
1157
950
|
});
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
951
|
+
}
|
|
952
|
+
if (subcommand === "activate") {
|
|
953
|
+
const id = cli.positionals[0];
|
|
954
|
+
if (!id)
|
|
955
|
+
throw new Error("models activate requires <model-id>");
|
|
956
|
+
const model = await store.getModel(id);
|
|
957
|
+
const verified = await verifyStoredModel(model);
|
|
958
|
+
await verifyActivationEvidence(model);
|
|
959
|
+
const pointer = await activateModel(store, model.id);
|
|
960
|
+
return printJson({
|
|
961
|
+
active: pointer.model_id,
|
|
962
|
+
pointer,
|
|
963
|
+
manifest_path: verified.manifest_path,
|
|
964
|
+
integrity: verified.integrity,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
if (subcommand === "rollback") {
|
|
968
|
+
const current = await getActiveModel(store);
|
|
969
|
+
if (current.pointer?.previous_model_id) {
|
|
970
|
+
const previous = await store.getModel(current.pointer.previous_model_id);
|
|
971
|
+
await verifyStoredModel(previous);
|
|
972
|
+
await verifyActivationEvidence(previous);
|
|
1162
973
|
}
|
|
974
|
+
const pointer = await rollbackActiveModel(store);
|
|
975
|
+
return printJson({ active: pointer.model_id ?? "base", pointer });
|
|
976
|
+
}
|
|
977
|
+
if (subcommand === "prefetch" || subcommand === "verify-base") {
|
|
978
|
+
const input = await loadCliBehaviorSpec(modelInputPath);
|
|
1163
979
|
const report = await prefetchBaseModel({
|
|
1164
980
|
request: input.request,
|
|
1165
981
|
config,
|
|
@@ -1171,7 +987,6 @@ async function main(argv) {
|
|
|
1171
987
|
});
|
|
1172
988
|
return printJson({
|
|
1173
989
|
...report,
|
|
1174
|
-
input_kind: input.kind,
|
|
1175
990
|
input_path: input.path,
|
|
1176
991
|
});
|
|
1177
992
|
}
|
|
@@ -1179,95 +994,11 @@ async function main(argv) {
|
|
|
1179
994
|
const id = cli.positionals[0];
|
|
1180
995
|
if (!id)
|
|
1181
996
|
throw new Error("models serve requires <model-id>");
|
|
1182
|
-
|
|
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);
|
|
997
|
+
await serveModelTargetFromCli({ argv, target: id, config });
|
|
1222
998
|
return;
|
|
1223
999
|
}
|
|
1224
1000
|
throw new Error(`Unknown models command: ${subcommand}`);
|
|
1225
1001
|
}
|
|
1226
|
-
if (command === "specs") {
|
|
1227
|
-
const subcommand = cli.subcommand;
|
|
1228
|
-
const config = await configFromArgv(argv);
|
|
1229
|
-
const store = createLocalStore(config.storeRoot);
|
|
1230
|
-
if (subcommand === "list")
|
|
1231
|
-
return printJson(await store.listSpecs());
|
|
1232
|
-
if (subcommand === "get") {
|
|
1233
|
-
const id = cli.positionals[0];
|
|
1234
|
-
if (!id)
|
|
1235
|
-
throw new Error("specs get requires <spec-id>");
|
|
1236
|
-
return printJson(await store.getSpec(id));
|
|
1237
|
-
}
|
|
1238
|
-
if (subcommand === "import") {
|
|
1239
|
-
const path = cli.positionals[0];
|
|
1240
|
-
if (!path)
|
|
1241
|
-
throw new Error("specs import requires <spec-or-request.json>");
|
|
1242
|
-
const input = JSON.parse(await readFile(resolve(path), "utf8"));
|
|
1243
|
-
const request = fineTuneRunRequestSchema.safeParse(input);
|
|
1244
|
-
if (request.success)
|
|
1245
|
-
return printJson(await store.importSpec(request.data.behavior_spec_id, request.data.spec_snapshot));
|
|
1246
|
-
const localSpec = localBehaviorSpecFileSchema.safeParse(input);
|
|
1247
|
-
if (localSpec.success) {
|
|
1248
|
-
const localRequest = runRequestFromLocalSpec(localSpec.data, {
|
|
1249
|
-
userId: readOption(argv, "--user-id"),
|
|
1250
|
-
runNumber: readNumberOption(argv, "--run-number"),
|
|
1251
|
-
});
|
|
1252
|
-
return printJson(await store.importSpec(localRequest.behavior_spec_id, localRequest.spec_snapshot));
|
|
1253
|
-
}
|
|
1254
|
-
const id = readOption(argv, "--id");
|
|
1255
|
-
if (!id)
|
|
1256
|
-
throw new Error("specs import requires --id when importing a raw spec snapshot");
|
|
1257
|
-
return printJson(await store.importSpec(id, specSnapshotSchema.parse(input)));
|
|
1258
|
-
}
|
|
1259
|
-
throw new Error(`Unknown specs command: ${subcommand}`);
|
|
1260
|
-
}
|
|
1261
|
-
if (command === "store") {
|
|
1262
|
-
const subcommand = cli.subcommand;
|
|
1263
|
-
const config = await configFromArgv(argv);
|
|
1264
|
-
const store = createLocalStore(config.storeRoot);
|
|
1265
|
-
if (subcommand === "rebuild-index") {
|
|
1266
|
-
await store.rebuildIndexes();
|
|
1267
|
-
return printJson({ ok: true });
|
|
1268
|
-
}
|
|
1269
|
-
throw new Error(`Unknown store command: ${subcommand ?? ""}`);
|
|
1270
|
-
}
|
|
1271
1002
|
console.error(`Unknown command: ${command}`);
|
|
1272
1003
|
process.exitCode = 1;
|
|
1273
1004
|
}
|