@tuned-tensor/cli 0.4.18 → 0.4.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/dist/index.js +552 -77
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -122,6 +122,9 @@ function post(path, body, opts) {
|
|
|
122
122
|
function put(path, body, opts) {
|
|
123
123
|
return request("PUT", path, { body, opts });
|
|
124
124
|
}
|
|
125
|
+
function patch(path, body, opts) {
|
|
126
|
+
return request("PATCH", path, { body, opts });
|
|
127
|
+
}
|
|
125
128
|
function del(path, opts) {
|
|
126
129
|
return request("DELETE", path, { opts });
|
|
127
130
|
}
|
|
@@ -297,10 +300,10 @@ var ResolveError = class extends Error {
|
|
|
297
300
|
function isFullUuid(value) {
|
|
298
301
|
return UUID_RE.test(value);
|
|
299
302
|
}
|
|
300
|
-
async function resolvePrefix(prefix, kind, listPath, describe, opts) {
|
|
303
|
+
async function resolvePrefix(prefix, kind, listPath, describe, opts, listCmdOverride) {
|
|
301
304
|
if (isFullUuid(prefix)) return prefix;
|
|
302
305
|
const noun = kind;
|
|
303
|
-
const listCmd = `tt ${kind}s list --json`;
|
|
306
|
+
const listCmd = listCmdOverride ?? `tt ${kind}s list --json`;
|
|
304
307
|
if (prefix.length < MIN_PREFIX_LEN) {
|
|
305
308
|
throw new ResolveError(
|
|
306
309
|
`${capitalize(noun)} ID prefix "${prefix}" is too short \u2014 provide at least ${MIN_PREFIX_LEN} characters or the full UUID.`
|
|
@@ -368,6 +371,16 @@ function resolveModelId(prefix, opts) {
|
|
|
368
371
|
opts
|
|
369
372
|
);
|
|
370
373
|
}
|
|
374
|
+
function resolveLabelingJobId(prefix, opts) {
|
|
375
|
+
return resolvePrefix(
|
|
376
|
+
prefix,
|
|
377
|
+
"labeling job",
|
|
378
|
+
"/labeling-jobs",
|
|
379
|
+
(j) => `${j.id}${j.name ? ` (${j.name})` : ""}`,
|
|
380
|
+
opts,
|
|
381
|
+
"tt label list --json"
|
|
382
|
+
);
|
|
383
|
+
}
|
|
371
384
|
|
|
372
385
|
// src/base-models.ts
|
|
373
386
|
var SUPPORTED_BASE_MODELS = [
|
|
@@ -617,6 +630,29 @@ function formatMinutes(minutes) {
|
|
|
617
630
|
if (minutes < 60) return `${Math.max(1, Math.round(minutes))}m`;
|
|
618
631
|
return `${(minutes / 60).toFixed(1)}h`;
|
|
619
632
|
}
|
|
633
|
+
function formatCents(cents) {
|
|
634
|
+
const dollars = cents / 100;
|
|
635
|
+
if (Math.abs(dollars) >= 100) return `$${dollars.toFixed(0)}`;
|
|
636
|
+
return `$${dollars.toFixed(2)}`;
|
|
637
|
+
}
|
|
638
|
+
function formatEstimateRange(estimate) {
|
|
639
|
+
const duration = estimate.duration;
|
|
640
|
+
return `${formatMinutes(duration.estimated_minutes)} (${formatMinutes(duration.range_minutes.low)}-${formatMinutes(duration.range_minutes.high)})`;
|
|
641
|
+
}
|
|
642
|
+
function printRunEstimate(estimate) {
|
|
643
|
+
printDetail([
|
|
644
|
+
["Estimated Time", formatEstimateRange(estimate)],
|
|
645
|
+
["Confidence", estimate.duration.confidence],
|
|
646
|
+
["History Samples", String(estimate.duration.sample_count)],
|
|
647
|
+
["Basis", estimate.duration.basis.replaceAll("_", " ")],
|
|
648
|
+
["Estimated Cost", formatCents(estimate.estimated_cost_cents)],
|
|
649
|
+
["Training Tokens", `${(estimate.estimated_training_tokens / 1e3).toFixed(1)}k`],
|
|
650
|
+
["Epochs", String(estimate.estimated_epochs)]
|
|
651
|
+
]);
|
|
652
|
+
console.log(
|
|
653
|
+
"\nDuration is a rough historical range. Final cost uses provider-reported training tokens."
|
|
654
|
+
);
|
|
655
|
+
}
|
|
620
656
|
function formatEpochProgress(diagnostics) {
|
|
621
657
|
const curve = diagnostics.training.curve;
|
|
622
658
|
if (curve.latest_epoch == null) return void 0;
|
|
@@ -722,6 +758,46 @@ function printEvalOutputDiagnostics(diagnostics) {
|
|
|
722
758
|
}
|
|
723
759
|
}
|
|
724
760
|
}
|
|
761
|
+
function addRunConfigurationOptions(command) {
|
|
762
|
+
return command.option("--no-augment", "Disable data augmentation").option("--no-llm-judge", "Disable LLM judging").option("--epochs <n>", "Number of training epochs").option("--lr <rate>", "Learning rate").option("--batch-size <n>", "Batch size").option("--dataset <id>", "Dataset ID to use instead of inline spec examples (full UUID or 4+ char prefix)").option("--parent-model <id>", "Fine-tuned model ID to continue training from (full UUID or 4+ char prefix)").option("--train-ratio <ratio>", "Dataset training split ratio (default: 0.8 when any split ratio is set)").option("--validation-ratio <ratio>", "Dataset validation split ratio (default: 0.1 when any split ratio is set)").option("--test-ratio <ratio>", "Dataset test split ratio (default: 0.1 when any split ratio is set)").option("--lora-rank <n>", "LoRA rank").option("--lora-alpha <n>", "LoRA alpha").option("--max-eval-examples <n>", "Max examples for the primary eval pass").option("--max-test-eval-examples <n>", "Max examples for the secondary test eval pass").option("--long-examples <policy>", "Long training row policy: error, truncate, or skip").option("--max-seq-length <tokens>", "Maximum training sequence length in tokens").option("--max-output-tokens <tokens>", "Desired evaluation output budget in tokens").option("--eval-reserved-output-tokens <tokens>", "Minimum evaluation output tokens reserved per row");
|
|
763
|
+
}
|
|
764
|
+
async function buildRunRequestBody(cmdOpts, opts) {
|
|
765
|
+
const body = {};
|
|
766
|
+
if (cmdOpts.augment === false) body.augment = false;
|
|
767
|
+
if (cmdOpts.dataset) body.dataset_id = await resolveDatasetId(String(cmdOpts.dataset), opts);
|
|
768
|
+
if (cmdOpts.parentModel) {
|
|
769
|
+
body.parent_model_id = await resolveModelId(String(cmdOpts.parentModel), opts);
|
|
770
|
+
}
|
|
771
|
+
const splitRatioOptions = [
|
|
772
|
+
cmdOpts.trainRatio,
|
|
773
|
+
cmdOpts.validationRatio,
|
|
774
|
+
cmdOpts.testRatio
|
|
775
|
+
];
|
|
776
|
+
if (splitRatioOptions.some((value) => value != null)) {
|
|
777
|
+
body.split_ratios = {
|
|
778
|
+
train: cmdOpts.trainRatio != null ? Number(cmdOpts.trainRatio) : 0.8,
|
|
779
|
+
validation: cmdOpts.validationRatio != null ? Number(cmdOpts.validationRatio) : 0.1,
|
|
780
|
+
test: cmdOpts.testRatio != null ? Number(cmdOpts.testRatio) : 0.1
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const hp = {};
|
|
784
|
+
if (cmdOpts.epochs) hp.n_epochs = Number(cmdOpts.epochs);
|
|
785
|
+
if (cmdOpts.lr) hp.learning_rate = Number(cmdOpts.lr);
|
|
786
|
+
if (cmdOpts.batchSize) hp.batch_size = Number(cmdOpts.batchSize);
|
|
787
|
+
if (cmdOpts.loraRank) hp.lora_rank = Number(cmdOpts.loraRank);
|
|
788
|
+
if (cmdOpts.loraAlpha) hp.lora_alpha = Number(cmdOpts.loraAlpha);
|
|
789
|
+
if (cmdOpts.maxEvalExamples) hp.max_eval_examples = Number(cmdOpts.maxEvalExamples);
|
|
790
|
+
if (cmdOpts.maxTestEvalExamples) hp.max_test_eval_examples = Number(cmdOpts.maxTestEvalExamples);
|
|
791
|
+
if (cmdOpts.longExamples) hp.long_examples = parseLongExamplesPolicy(String(cmdOpts.longExamples));
|
|
792
|
+
if (cmdOpts.maxSeqLength) hp.max_seq_length = Number(cmdOpts.maxSeqLength);
|
|
793
|
+
if (cmdOpts.maxOutputTokens) hp.max_output_tokens = Number(cmdOpts.maxOutputTokens);
|
|
794
|
+
if (cmdOpts.evalReservedOutputTokens) {
|
|
795
|
+
hp.eval_reserved_output_tokens = Number(cmdOpts.evalReservedOutputTokens);
|
|
796
|
+
}
|
|
797
|
+
if (cmdOpts.llmJudge === false) body.use_llm_judge = false;
|
|
798
|
+
if (Object.keys(hp).length) body.hyperparameters = hp;
|
|
799
|
+
return body;
|
|
800
|
+
}
|
|
725
801
|
function registerRunsCommands(parent) {
|
|
726
802
|
const runs = parent.command("runs").description("Manage runs");
|
|
727
803
|
runs.command("list").description("List runs").option("-s, --spec <id>", "Filter by spec ID (full UUID or 8+ char prefix)").option("-p, --page <n>", "Page number", "1").option("--per-page <n>", "Results per page", "20").action(async (cmdOpts) => {
|
|
@@ -801,42 +877,25 @@ Eval Results (${data._evals.length}):`);
|
|
|
801
877
|
);
|
|
802
878
|
}
|
|
803
879
|
});
|
|
804
|
-
|
|
880
|
+
addRunConfigurationOptions(
|
|
881
|
+
runs.command("estimate").description("Estimate run cost and duration before starting").argument("<spec-id>", "Behaviour spec ID (full UUID or 8+ char prefix)")
|
|
882
|
+
).action(async (specId, cmdOpts) => {
|
|
805
883
|
const opts = parent.opts();
|
|
806
|
-
const body =
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
body
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
test: cmdOpts.testRatio != null ? Number(cmdOpts.testRatio) : 0.1
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
const hp = {};
|
|
825
|
-
if (cmdOpts.epochs) hp.n_epochs = Number(cmdOpts.epochs);
|
|
826
|
-
if (cmdOpts.lr) hp.learning_rate = Number(cmdOpts.lr);
|
|
827
|
-
if (cmdOpts.batchSize) hp.batch_size = Number(cmdOpts.batchSize);
|
|
828
|
-
if (cmdOpts.loraRank) hp.lora_rank = Number(cmdOpts.loraRank);
|
|
829
|
-
if (cmdOpts.loraAlpha) hp.lora_alpha = Number(cmdOpts.loraAlpha);
|
|
830
|
-
if (cmdOpts.maxEvalExamples) hp.max_eval_examples = Number(cmdOpts.maxEvalExamples);
|
|
831
|
-
if (cmdOpts.maxTestEvalExamples) hp.max_test_eval_examples = Number(cmdOpts.maxTestEvalExamples);
|
|
832
|
-
if (cmdOpts.longExamples) hp.long_examples = parseLongExamplesPolicy(cmdOpts.longExamples);
|
|
833
|
-
if (cmdOpts.maxSeqLength) hp.max_seq_length = Number(cmdOpts.maxSeqLength);
|
|
834
|
-
if (cmdOpts.maxOutputTokens) hp.max_output_tokens = Number(cmdOpts.maxOutputTokens);
|
|
835
|
-
if (cmdOpts.evalReservedOutputTokens) {
|
|
836
|
-
hp.eval_reserved_output_tokens = Number(cmdOpts.evalReservedOutputTokens);
|
|
837
|
-
}
|
|
838
|
-
if (cmdOpts.llmJudge === false) body.use_llm_judge = false;
|
|
839
|
-
if (Object.keys(hp).length) body.hyperparameters = hp;
|
|
884
|
+
const body = await buildRunRequestBody(cmdOpts, opts);
|
|
885
|
+
const fullSpecId = await resolveSpecId(specId, opts);
|
|
886
|
+
const { data } = await post(
|
|
887
|
+
`/behavior-specs/${fullSpecId}/runs/estimate`,
|
|
888
|
+
body,
|
|
889
|
+
opts
|
|
890
|
+
);
|
|
891
|
+
if (isJsonMode()) return printJson(data);
|
|
892
|
+
printRunEstimate(data);
|
|
893
|
+
});
|
|
894
|
+
addRunConfigurationOptions(
|
|
895
|
+
runs.command("start").description("Start a new run for a behaviour spec").argument("<spec-id>", "Behaviour spec ID (full UUID or 8+ char prefix)")
|
|
896
|
+
).action(async (specId, cmdOpts) => {
|
|
897
|
+
const opts = parent.opts();
|
|
898
|
+
const body = await buildRunRequestBody(cmdOpts, opts);
|
|
840
899
|
const fullSpecId = await resolveSpecId(specId, opts);
|
|
841
900
|
const { data } = await post(
|
|
842
901
|
`/behavior-specs/${fullSpecId}/runs`,
|
|
@@ -1107,27 +1166,425 @@ function registerDatasetsCommands(parent) {
|
|
|
1107
1166
|
});
|
|
1108
1167
|
}
|
|
1109
1168
|
|
|
1169
|
+
// src/commands/label.ts
|
|
1170
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
1171
|
+
function formatCents2(cents) {
|
|
1172
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
1173
|
+
}
|
|
1174
|
+
function sleep(ms) {
|
|
1175
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1176
|
+
}
|
|
1177
|
+
function parseCsvHeader(line) {
|
|
1178
|
+
const fields = [];
|
|
1179
|
+
let current = "";
|
|
1180
|
+
let inQuotes = false;
|
|
1181
|
+
for (let i = 0; i < line.length; i++) {
|
|
1182
|
+
const ch = line[i];
|
|
1183
|
+
if (inQuotes) {
|
|
1184
|
+
if (ch === '"') {
|
|
1185
|
+
if (line[i + 1] === '"') {
|
|
1186
|
+
current += '"';
|
|
1187
|
+
i++;
|
|
1188
|
+
} else {
|
|
1189
|
+
inQuotes = false;
|
|
1190
|
+
}
|
|
1191
|
+
} else {
|
|
1192
|
+
current += ch;
|
|
1193
|
+
}
|
|
1194
|
+
} else if (ch === '"') {
|
|
1195
|
+
inQuotes = true;
|
|
1196
|
+
} else if (ch === ",") {
|
|
1197
|
+
fields.push(current);
|
|
1198
|
+
current = "";
|
|
1199
|
+
} else {
|
|
1200
|
+
current += ch;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
fields.push(current);
|
|
1204
|
+
return fields.map((f) => f.trim());
|
|
1205
|
+
}
|
|
1206
|
+
function validateUnlabeledFile(file, inputColumn) {
|
|
1207
|
+
const lower = file.toLowerCase();
|
|
1208
|
+
if (lower.endsWith(".csv")) {
|
|
1209
|
+
if (!inputColumn) {
|
|
1210
|
+
throw new Error(
|
|
1211
|
+
"CSV uploads need --input-column <name> to say which column holds the input text."
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
const firstLine = readFileSync5(file, "utf8").split(/\r?\n/, 1)[0] ?? "";
|
|
1215
|
+
const columns = parseCsvHeader(firstLine);
|
|
1216
|
+
if (!columns.includes(inputColumn)) {
|
|
1217
|
+
throw new Error(
|
|
1218
|
+
`Column "${inputColumn}" not found in CSV header. Available columns: ${columns.join(", ")}`
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
return { format: "csv" };
|
|
1222
|
+
}
|
|
1223
|
+
if (!lower.endsWith(".jsonl") && !lower.endsWith(".json")) {
|
|
1224
|
+
throw new Error("Labeling source file must be .jsonl or .csv");
|
|
1225
|
+
}
|
|
1226
|
+
const lines = readFileSync5(file, "utf8").split(/\r?\n/);
|
|
1227
|
+
const errors = [];
|
|
1228
|
+
let rowCount = 0;
|
|
1229
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
1230
|
+
const line = rawLine.trim();
|
|
1231
|
+
if (!line) continue;
|
|
1232
|
+
rowCount += 1;
|
|
1233
|
+
let row;
|
|
1234
|
+
try {
|
|
1235
|
+
row = JSON.parse(line);
|
|
1236
|
+
} catch {
|
|
1237
|
+
errors.push(`Row ${index + 1}: invalid JSON`);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.input !== "string") {
|
|
1241
|
+
errors.push(`Row ${index + 1}: expected an object with a string "input" field`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (rowCount === 0) {
|
|
1245
|
+
errors.push("File contains no JSONL rows");
|
|
1246
|
+
}
|
|
1247
|
+
if (errors.length > 0) {
|
|
1248
|
+
const preview = errors.slice(0, 5).join("\n");
|
|
1249
|
+
const suffix = errors.length > 5 ? `
|
|
1250
|
+
...and ${errors.length - 5} more error(s)` : "";
|
|
1251
|
+
throw new Error(
|
|
1252
|
+
`Invalid labeling source. Each JSONL row must be {"input": "..."}.
|
|
1253
|
+
${preview}${suffix}`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
return { format: "jsonl" };
|
|
1257
|
+
}
|
|
1258
|
+
function printJobDetail(job, counts) {
|
|
1259
|
+
printDetail([
|
|
1260
|
+
["ID", job.id],
|
|
1261
|
+
["Name", job.name],
|
|
1262
|
+
["Status", formatStatus(job.status)],
|
|
1263
|
+
["Spec", shortId(job.behavior_spec_id)],
|
|
1264
|
+
["Teacher", job.teacher_model],
|
|
1265
|
+
["Rows", String(job.row_count)],
|
|
1266
|
+
["Labeled", String(job.labeled_count)],
|
|
1267
|
+
["Failed", String(job.failed_count)],
|
|
1268
|
+
[
|
|
1269
|
+
"Cost",
|
|
1270
|
+
job.actual_cost_cents !== null ? formatCents2(job.actual_cost_cents) : `~${formatCents2(job.est_cost_cents)} (estimated)`
|
|
1271
|
+
],
|
|
1272
|
+
["Dataset", job.promoted_dataset_id ? shortId(job.promoted_dataset_id) : void 0],
|
|
1273
|
+
["Error", job.error ?? void 0],
|
|
1274
|
+
["Created", formatDate(job.created_at)]
|
|
1275
|
+
]);
|
|
1276
|
+
if (counts) {
|
|
1277
|
+
console.log(
|
|
1278
|
+
`
|
|
1279
|
+
Review: ${counts.labeled} awaiting \xB7 ${counts.accepted} accepted \xB7 ${counts.edited} edited \xB7 ${counts.rejected} rejected \xB7 ${counts.failed} failed`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async function watchJob(jobId, opts) {
|
|
1284
|
+
for (; ; ) {
|
|
1285
|
+
const { data } = await get(
|
|
1286
|
+
`/labeling-jobs/${jobId}`,
|
|
1287
|
+
void 0,
|
|
1288
|
+
opts
|
|
1289
|
+
);
|
|
1290
|
+
if (!isJsonMode()) {
|
|
1291
|
+
if (data.status === "preparing") {
|
|
1292
|
+
process.stdout.write("\rPreparing - parsing the source file... ");
|
|
1293
|
+
} else {
|
|
1294
|
+
const done = data.counts.total - data.counts.pending;
|
|
1295
|
+
process.stdout.write(`\rLabeling ${done}/${data.counts.total} rows `);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (data.status !== "preparing" && data.status !== "labeling") {
|
|
1299
|
+
if (!isJsonMode()) process.stdout.write("\n");
|
|
1300
|
+
return data;
|
|
1301
|
+
}
|
|
1302
|
+
await sleep(5e3);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
async function resolveRowIds(jobId, indexes, opts) {
|
|
1306
|
+
const PER_PAGE = 100;
|
|
1307
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
1308
|
+
for (const index of indexes) {
|
|
1309
|
+
const page = Math.floor(index / PER_PAGE) + 1;
|
|
1310
|
+
byPage.set(page, [...byPage.get(page) ?? [], index]);
|
|
1311
|
+
}
|
|
1312
|
+
const found = /* @__PURE__ */ new Map();
|
|
1313
|
+
for (const [page, wanted] of byPage) {
|
|
1314
|
+
const { data } = await get(
|
|
1315
|
+
`/labeling-jobs/${jobId}/rows`,
|
|
1316
|
+
{ page, per_page: PER_PAGE },
|
|
1317
|
+
opts
|
|
1318
|
+
);
|
|
1319
|
+
for (const row of data) {
|
|
1320
|
+
if (wanted.includes(row.row_index)) {
|
|
1321
|
+
found.set(row.row_index, row.id);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const missing = indexes.filter((i) => !found.has(i));
|
|
1326
|
+
if (missing.length > 0) {
|
|
1327
|
+
throw new Error(`Row index(es) not found: ${missing.join(", ")}`);
|
|
1328
|
+
}
|
|
1329
|
+
return found;
|
|
1330
|
+
}
|
|
1331
|
+
function parseIndexList(value) {
|
|
1332
|
+
const indexes = value.split(",").map((part) => Number.parseInt(part.trim(), 10));
|
|
1333
|
+
if (indexes.some((n) => Number.isNaN(n) || n < 0)) {
|
|
1334
|
+
throw new Error(`Invalid row index list: "${value}" (expected e.g. "0,3,12")`);
|
|
1335
|
+
}
|
|
1336
|
+
return indexes;
|
|
1337
|
+
}
|
|
1338
|
+
function registerLabelCommands(parent) {
|
|
1339
|
+
const label = parent.command("label").description("Teacher-label real data into training datasets");
|
|
1340
|
+
label.command("upload").description("Upload unlabeled inputs (.jsonl or .csv) and start a labeling job").argument("<file>", 'Path to JSONL ({"input": ...} per line) or CSV file').requiredOption("-s, --spec <id>", "Behaviour spec the teacher labels under").option("-c, --input-column <name>", "CSV column that holds the input text").option("-n, --name <name>", "Job name (defaults to filename)").option("--watch", "Block and show progress until labeling finishes").action(async (file, cmdOpts) => {
|
|
1341
|
+
const opts = parent.opts();
|
|
1342
|
+
if (!existsSync3(file)) {
|
|
1343
|
+
printError(`File not found: ${file}`);
|
|
1344
|
+
process.exit(1);
|
|
1345
|
+
}
|
|
1346
|
+
const { format } = validateUnlabeledFile(file, cmdOpts.inputColumn);
|
|
1347
|
+
const specId = await resolveSpecId(cmdOpts.spec, opts);
|
|
1348
|
+
const name = cmdOpts.name || file.split("/").pop().replace(/\.(jsonl|json|csv)$/, "");
|
|
1349
|
+
const fileBytes = readFileSync5(file);
|
|
1350
|
+
const contentType = format === "csv" ? "text/csv" : "application/jsonl";
|
|
1351
|
+
const { data: uploadUrl } = await post(
|
|
1352
|
+
"/labeling-jobs/upload-url",
|
|
1353
|
+
{
|
|
1354
|
+
filename: file.split("/").pop(),
|
|
1355
|
+
size: statSync3(file).size,
|
|
1356
|
+
contentType
|
|
1357
|
+
},
|
|
1358
|
+
opts
|
|
1359
|
+
);
|
|
1360
|
+
const uploadRes = await fetch(uploadUrl.upload_url, {
|
|
1361
|
+
method: uploadUrl.method,
|
|
1362
|
+
headers: uploadUrl.headers ?? { "Content-Type": contentType },
|
|
1363
|
+
body: new Blob([fileBytes], { type: contentType })
|
|
1364
|
+
});
|
|
1365
|
+
if (!uploadRes.ok) {
|
|
1366
|
+
throw new Error(`Upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
|
|
1367
|
+
}
|
|
1368
|
+
const { data: job } = await post(
|
|
1369
|
+
"/labeling-jobs",
|
|
1370
|
+
{
|
|
1371
|
+
path: uploadUrl.path,
|
|
1372
|
+
name,
|
|
1373
|
+
behavior_spec_id: specId,
|
|
1374
|
+
source_format: format,
|
|
1375
|
+
input_column: cmdOpts.inputColumn ?? null
|
|
1376
|
+
},
|
|
1377
|
+
opts
|
|
1378
|
+
);
|
|
1379
|
+
if (!cmdOpts.watch) {
|
|
1380
|
+
if (isJsonMode()) return printJson(job);
|
|
1381
|
+
printSuccess(
|
|
1382
|
+
`Labeling started: ${job.name} (${shortId(job.id)}) \u2014 est. ${formatCents2(job.est_cost_cents)}. Runs in the cloud; no need to stay connected.`
|
|
1383
|
+
);
|
|
1384
|
+
console.log(`Follow progress with \`tt label watch ${shortId(job.id)}\`.`);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const finished = await watchJob(job.id, opts);
|
|
1388
|
+
if (isJsonMode()) return printJson(finished);
|
|
1389
|
+
printSuccess(
|
|
1390
|
+
`Labeling ${finished.status === "awaiting_review" ? "complete" : finished.status}: ${finished.labeled_count}/${finished.row_count} rows labeled.`
|
|
1391
|
+
);
|
|
1392
|
+
console.log(
|
|
1393
|
+
`Review with \`tt label rows ${shortId(job.id)} --status labeled\`, then \`tt label promote ${shortId(job.id)} --name <dataset-name>\`.`
|
|
1394
|
+
);
|
|
1395
|
+
});
|
|
1396
|
+
label.command("watch").description("Watch a labeling job until it is ready for review").argument("<id>", "Labeling job ID (full UUID or 4+ char prefix)").action(async (id) => {
|
|
1397
|
+
const opts = parent.opts();
|
|
1398
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1399
|
+
const job = await watchJob(jobId, opts);
|
|
1400
|
+
if (isJsonMode()) return printJson(job);
|
|
1401
|
+
if (job.status === "awaiting_review") {
|
|
1402
|
+
printSuccess(
|
|
1403
|
+
`Labeling complete: ${job.labeled_count}/${job.row_count} rows labeled, cost ${job.actual_cost_cents !== null ? formatCents2(job.actual_cost_cents) : "n/a"}.`
|
|
1404
|
+
);
|
|
1405
|
+
} else {
|
|
1406
|
+
printError(`Labeling ended with status: ${job.status}${job.error ? ` \u2014 ${job.error}` : ""}`);
|
|
1407
|
+
process.exitCode = 1;
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
label.command("list").description("List labeling jobs").option("-p, --page <n>", "Page number", "1").option("--per-page <n>", "Results per page", "20").action(async (cmdOpts) => {
|
|
1411
|
+
const opts = parent.opts();
|
|
1412
|
+
const { data, meta } = await get(
|
|
1413
|
+
"/labeling-jobs",
|
|
1414
|
+
{ page: cmdOpts.page, per_page: cmdOpts.perPage },
|
|
1415
|
+
opts
|
|
1416
|
+
);
|
|
1417
|
+
if (isJsonMode()) return printJson({ data, meta });
|
|
1418
|
+
printTable(
|
|
1419
|
+
["ID", "Name", "Status", "Progress", "Cost", "Created"],
|
|
1420
|
+
data.map((j) => [
|
|
1421
|
+
shortId(j.id),
|
|
1422
|
+
truncate(j.name, 30),
|
|
1423
|
+
formatStatus(j.status),
|
|
1424
|
+
`${j.labeled_count}/${j.row_count}`,
|
|
1425
|
+
j.actual_cost_cents !== null ? formatCents2(j.actual_cost_cents) : `~${formatCents2(j.est_cost_cents)}`,
|
|
1426
|
+
formatDate(j.created_at)
|
|
1427
|
+
]),
|
|
1428
|
+
meta
|
|
1429
|
+
);
|
|
1430
|
+
});
|
|
1431
|
+
label.command("status").description("Show labeling job details and review progress").argument("<id>", "Labeling job ID (full UUID or 4+ char prefix)").action(async (id) => {
|
|
1432
|
+
const opts = parent.opts();
|
|
1433
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1434
|
+
const { data } = await get(
|
|
1435
|
+
`/labeling-jobs/${jobId}`,
|
|
1436
|
+
void 0,
|
|
1437
|
+
opts
|
|
1438
|
+
);
|
|
1439
|
+
if (isJsonMode()) return printJson(data);
|
|
1440
|
+
printJobDetail(data, data.counts);
|
|
1441
|
+
});
|
|
1442
|
+
label.command("rows").description("List rows in a labeling job").argument("<id>", "Labeling job ID (full UUID or 4+ char prefix)").option(
|
|
1443
|
+
"--status <status>",
|
|
1444
|
+
"Filter by row status (pending|labeled|accepted|edited|rejected|failed)"
|
|
1445
|
+
).option("-p, --page <n>", "Page number", "1").option("--per-page <n>", "Results per page", "50").action(async (id, cmdOpts) => {
|
|
1446
|
+
const opts = parent.opts();
|
|
1447
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1448
|
+
const query = {
|
|
1449
|
+
page: cmdOpts.page,
|
|
1450
|
+
per_page: cmdOpts.perPage
|
|
1451
|
+
};
|
|
1452
|
+
if (cmdOpts.status) query.status = cmdOpts.status;
|
|
1453
|
+
const { data, meta } = await get(
|
|
1454
|
+
`/labeling-jobs/${jobId}/rows`,
|
|
1455
|
+
query,
|
|
1456
|
+
opts
|
|
1457
|
+
);
|
|
1458
|
+
if (isJsonMode()) return printJson({ data, meta });
|
|
1459
|
+
printTable(
|
|
1460
|
+
["Row", "Status", "Input", "Output"],
|
|
1461
|
+
data.map((row) => [
|
|
1462
|
+
String(row.row_index),
|
|
1463
|
+
formatStatus(row.status),
|
|
1464
|
+
truncate(row.input.replace(/\s+/g, " "), 40),
|
|
1465
|
+
truncate(
|
|
1466
|
+
(row.final_output ?? row.teacher_output ?? "\u2014").replace(/\s+/g, " "),
|
|
1467
|
+
40
|
|
1468
|
+
)
|
|
1469
|
+
]),
|
|
1470
|
+
meta
|
|
1471
|
+
);
|
|
1472
|
+
});
|
|
1473
|
+
label.command("accept").description("Accept teacher-labeled rows").argument("<id>", "Labeling job ID (full UUID or 4+ char prefix)").option("--all", "Accept all unreviewed labeled rows").option("--rows <indexes>", "Comma-separated row indexes (e.g. 0,3,12)").action(async (id, cmdOpts) => {
|
|
1474
|
+
const opts = parent.opts();
|
|
1475
|
+
if (!cmdOpts.all && !cmdOpts.rows) {
|
|
1476
|
+
printError("Provide --all or --rows <indexes>.");
|
|
1477
|
+
process.exit(1);
|
|
1478
|
+
}
|
|
1479
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1480
|
+
if (cmdOpts.all) {
|
|
1481
|
+
const { data } = await post(
|
|
1482
|
+
`/labeling-jobs/${jobId}/rows/bulk`,
|
|
1483
|
+
{ action: "accept", all_labeled: true },
|
|
1484
|
+
opts
|
|
1485
|
+
);
|
|
1486
|
+
if (isJsonMode()) return printJson(data);
|
|
1487
|
+
return printSuccess(`Accepted ${data.accepted} rows.`);
|
|
1488
|
+
}
|
|
1489
|
+
const indexes = parseIndexList(cmdOpts.rows);
|
|
1490
|
+
const rowIds = await resolveRowIds(jobId, indexes, opts);
|
|
1491
|
+
for (const index of indexes) {
|
|
1492
|
+
await patch(
|
|
1493
|
+
`/labeling-jobs/${jobId}/rows/${rowIds.get(index)}`,
|
|
1494
|
+
{ action: "accept" },
|
|
1495
|
+
opts
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
if (isJsonMode()) return printJson({ accepted: indexes.length });
|
|
1499
|
+
printSuccess(`Accepted ${indexes.length} row(s).`);
|
|
1500
|
+
});
|
|
1501
|
+
label.command("reject").description("Reject rows so they are excluded from promotion").argument("<id>", "Labeling job ID (full UUID or 4+ char prefix)").requiredOption("--rows <indexes>", "Comma-separated row indexes (e.g. 0,3,12)").action(async (id, cmdOpts) => {
|
|
1502
|
+
const opts = parent.opts();
|
|
1503
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1504
|
+
const indexes = parseIndexList(cmdOpts.rows);
|
|
1505
|
+
const rowIds = await resolveRowIds(jobId, indexes, opts);
|
|
1506
|
+
for (const index of indexes) {
|
|
1507
|
+
await patch(
|
|
1508
|
+
`/labeling-jobs/${jobId}/rows/${rowIds.get(index)}`,
|
|
1509
|
+
{ action: "reject" },
|
|
1510
|
+
opts
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
if (isJsonMode()) return printJson({ rejected: indexes.length });
|
|
1514
|
+
printSuccess(`Rejected ${indexes.length} row(s).`);
|
|
1515
|
+
});
|
|
1516
|
+
label.command("edit").description("Replace a row's output with your own").argument("<id>", "Labeling job ID (full UUID or 4+ char prefix)").requiredOption("--row <index>", "Row index to edit").option("-o, --output <text>", "Replacement output text").option("-f, --file <path>", "Read replacement output from a file").action(async (id, cmdOpts) => {
|
|
1517
|
+
const opts = parent.opts();
|
|
1518
|
+
if (!cmdOpts.output && !cmdOpts.file) {
|
|
1519
|
+
printError("Provide --output <text> or --file <path>.");
|
|
1520
|
+
process.exit(1);
|
|
1521
|
+
}
|
|
1522
|
+
const output = cmdOpts.output ?? readFileSync5(cmdOpts.file, "utf8");
|
|
1523
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1524
|
+
const indexes = parseIndexList(cmdOpts.row);
|
|
1525
|
+
const rowIds = await resolveRowIds(jobId, indexes, opts);
|
|
1526
|
+
await patch(
|
|
1527
|
+
`/labeling-jobs/${jobId}/rows/${rowIds.get(indexes[0])}`,
|
|
1528
|
+
{ action: "edit", output },
|
|
1529
|
+
opts
|
|
1530
|
+
);
|
|
1531
|
+
if (isJsonMode()) return printJson({ edited: indexes[0] });
|
|
1532
|
+
printSuccess(`Row ${indexes[0]} edited.`);
|
|
1533
|
+
});
|
|
1534
|
+
label.command("promote").description("Promote reviewed rows into a validated dataset").argument("<id>", "Labeling job ID (full UUID or 4+ char prefix)").requiredOption("-n, --name <name>", "Name for the new dataset").option("-d, --description <desc>", "Dataset description").option("--include-unreviewed", "Also include unreviewed labeled rows").action(async (id, cmdOpts) => {
|
|
1535
|
+
const opts = parent.opts();
|
|
1536
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1537
|
+
const { data } = await post(
|
|
1538
|
+
`/labeling-jobs/${jobId}/promote`,
|
|
1539
|
+
{
|
|
1540
|
+
name: cmdOpts.name,
|
|
1541
|
+
description: cmdOpts.description ?? null,
|
|
1542
|
+
include_unreviewed_labeled: Boolean(cmdOpts.includeUnreviewed)
|
|
1543
|
+
},
|
|
1544
|
+
opts
|
|
1545
|
+
);
|
|
1546
|
+
if (isJsonMode()) return printJson(data);
|
|
1547
|
+
printSuccess(
|
|
1548
|
+
`Dataset created: ${data.name} (${shortId(data.id)}) \u2014 ${data.row_count} rows.`
|
|
1549
|
+
);
|
|
1550
|
+
console.log(
|
|
1551
|
+
`Start a run with \`tt runs start <spec-id> --dataset ${shortId(data.id)}\`.`
|
|
1552
|
+
);
|
|
1553
|
+
});
|
|
1554
|
+
label.command("cancel").description("Cancel a labeling job and release unused credits").argument("<id>", "Labeling job ID (full UUID or 4+ char prefix)").action(async (id) => {
|
|
1555
|
+
const opts = parent.opts();
|
|
1556
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1557
|
+
const { data } = await post(
|
|
1558
|
+
`/labeling-jobs/${jobId}/cancel`,
|
|
1559
|
+
{},
|
|
1560
|
+
opts
|
|
1561
|
+
);
|
|
1562
|
+
if (isJsonMode()) return printJson(data);
|
|
1563
|
+
printSuccess(`Labeling job cancelled: ${shortId(jobId)}`);
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1110
1567
|
// src/commands/models.ts
|
|
1111
1568
|
import { spawn as spawn2, execFileSync } from "child_process";
|
|
1112
1569
|
import { createHash } from "crypto";
|
|
1113
1570
|
import {
|
|
1114
|
-
existsSync as
|
|
1571
|
+
existsSync as existsSync5,
|
|
1115
1572
|
mkdirSync as mkdirSync3,
|
|
1116
|
-
statSync as
|
|
1573
|
+
statSync as statSync4,
|
|
1117
1574
|
createWriteStream,
|
|
1118
1575
|
unlinkSync,
|
|
1119
|
-
readFileSync as
|
|
1576
|
+
readFileSync as readFileSync7,
|
|
1120
1577
|
writeFileSync as writeFileSync3,
|
|
1121
1578
|
readdirSync,
|
|
1122
1579
|
rmSync
|
|
1123
1580
|
} from "fs";
|
|
1124
1581
|
import { homedir as homedir2 } from "os";
|
|
1125
|
-
import { dirname as dirname2, join as join2, resolve as resolve2, basename as basename2 } from "path";
|
|
1582
|
+
import { dirname as dirname2, join as join2, resolve as resolve2, basename as basename2, normalize, isAbsolute } from "path";
|
|
1126
1583
|
import { Readable, Transform } from "stream";
|
|
1127
1584
|
import { pipeline } from "stream/promises";
|
|
1128
1585
|
|
|
1129
1586
|
// src/commands/init.ts
|
|
1130
|
-
import { existsSync as
|
|
1587
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
|
|
1131
1588
|
import { resolve } from "path";
|
|
1132
1589
|
var DEFAULT_SPEC_FILE = "tunedtensor.json";
|
|
1133
1590
|
var SCAFFOLD = {
|
|
@@ -1145,7 +1602,7 @@ var SCAFFOLD = {
|
|
|
1145
1602
|
function registerInitCommand(parent) {
|
|
1146
1603
|
parent.command("init").description("Create a local behaviour spec file").option("-n, --name <name>", "Spec name").option("--model <model>", "Base model ID").option("-f, --file <path>", "Output file path", DEFAULT_SPEC_FILE).action(async (cmdOpts) => {
|
|
1147
1604
|
const filePath = resolve(cmdOpts.file);
|
|
1148
|
-
if (
|
|
1605
|
+
if (existsSync4(filePath)) {
|
|
1149
1606
|
printWarning(`${cmdOpts.file} already exists. Use tt eval to validate it or edit it directly.`);
|
|
1150
1607
|
return;
|
|
1151
1608
|
}
|
|
@@ -1162,14 +1619,14 @@ function registerInitCommand(parent) {
|
|
|
1162
1619
|
}
|
|
1163
1620
|
function loadSpec(filePath) {
|
|
1164
1621
|
const resolved = resolve(filePath || DEFAULT_SPEC_FILE);
|
|
1165
|
-
if (!
|
|
1622
|
+
if (!existsSync4(resolved)) {
|
|
1166
1623
|
printError(
|
|
1167
1624
|
`Spec file not found: ${filePath || DEFAULT_SPEC_FILE}
|
|
1168
1625
|
Run \`tt init\` to create one.`
|
|
1169
1626
|
);
|
|
1170
1627
|
process.exit(1);
|
|
1171
1628
|
}
|
|
1172
|
-
const raw = JSON.parse(
|
|
1629
|
+
const raw = JSON.parse(readFileSync6(resolved, "utf-8"));
|
|
1173
1630
|
return raw;
|
|
1174
1631
|
}
|
|
1175
1632
|
|
|
@@ -1184,7 +1641,7 @@ var COMPLETION_ROUTES = /* @__PURE__ */ new Set(["/v1/chat/completions", "/chat/
|
|
|
1184
1641
|
function parsePositiveInteger(value, fallback) {
|
|
1185
1642
|
return Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
1186
1643
|
}
|
|
1187
|
-
function
|
|
1644
|
+
function sleep2(ms) {
|
|
1188
1645
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1189
1646
|
}
|
|
1190
1647
|
function readRequestBody(req) {
|
|
@@ -1506,7 +1963,7 @@ var ManagedModelServer = class {
|
|
|
1506
1963
|
child.kill("SIGTERM");
|
|
1507
1964
|
await Promise.race([
|
|
1508
1965
|
new Promise((resolve4) => child.once("exit", () => resolve4())),
|
|
1509
|
-
|
|
1966
|
+
sleep2(5e3).then(() => {
|
|
1510
1967
|
if (child.exitCode == null && child.signalCode == null) child.kill("SIGKILL");
|
|
1511
1968
|
})
|
|
1512
1969
|
]);
|
|
@@ -1536,7 +1993,7 @@ var ManagedModelServer = class {
|
|
|
1536
1993
|
const health = await this.checkHealth();
|
|
1537
1994
|
if (health.ok) return;
|
|
1538
1995
|
if (!this.child) break;
|
|
1539
|
-
await
|
|
1996
|
+
await sleep2(50);
|
|
1540
1997
|
}
|
|
1541
1998
|
throw new Error("Model server did not become healthy within 120 seconds");
|
|
1542
1999
|
}
|
|
@@ -2045,7 +2502,7 @@ if __name__ == "__main__":
|
|
|
2045
2502
|
`;
|
|
2046
2503
|
function resolveOutputPath(output, filename) {
|
|
2047
2504
|
if (!output) return filename;
|
|
2048
|
-
if (
|
|
2505
|
+
if (existsSync5(output) && statSync4(output).isDirectory()) {
|
|
2049
2506
|
return join2(output, filename);
|
|
2050
2507
|
}
|
|
2051
2508
|
return output;
|
|
@@ -2166,7 +2623,7 @@ function runtimePythonPath(cacheDir) {
|
|
|
2166
2623
|
function resolveServePython(cacheDir, explicitPython) {
|
|
2167
2624
|
if (explicitPython) return explicitPython;
|
|
2168
2625
|
const managedPython = runtimePythonPath(cacheDir);
|
|
2169
|
-
return
|
|
2626
|
+
return existsSync5(managedPython) ? managedPython : "python3";
|
|
2170
2627
|
}
|
|
2171
2628
|
function commandExists(command) {
|
|
2172
2629
|
try {
|
|
@@ -2245,11 +2702,28 @@ function safeSegment(value) {
|
|
|
2245
2702
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "model";
|
|
2246
2703
|
}
|
|
2247
2704
|
function directoryHasFiles(path) {
|
|
2248
|
-
return
|
|
2705
|
+
return existsSync5(path) && statSync4(path).isDirectory() && readdirSync(path).length > 0;
|
|
2706
|
+
}
|
|
2707
|
+
function validateArchiveEntryPath(entry) {
|
|
2708
|
+
const normalized = normalize(entry).replace(/^[\\/]+/, "");
|
|
2709
|
+
if (!entry.trim() || isAbsolute(entry) || normalized === ".." || normalized.startsWith(`..${"/"}`) || normalized.startsWith(`..${"\\"}`)) {
|
|
2710
|
+
throw new Error(`Unsafe archive entry path: ${entry}`);
|
|
2711
|
+
}
|
|
2712
|
+
return normalized;
|
|
2713
|
+
}
|
|
2714
|
+
function validateArchiveEntries(archivePath) {
|
|
2715
|
+
const listing = execFileSync("tar", ["-tzf", archivePath], {
|
|
2716
|
+
encoding: "utf8",
|
|
2717
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2718
|
+
});
|
|
2719
|
+
for (const entry of listing.split(/\r?\n/)) {
|
|
2720
|
+
if (entry.trim()) validateArchiveEntryPath(entry);
|
|
2721
|
+
}
|
|
2249
2722
|
}
|
|
2250
2723
|
function extractArchive(archivePath, targetDir, force) {
|
|
2251
2724
|
if (directoryHasFiles(targetDir) && !force) return targetDir;
|
|
2252
|
-
|
|
2725
|
+
validateArchiveEntries(archivePath);
|
|
2726
|
+
if (existsSync5(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
2253
2727
|
mkdirSync3(targetDir, { recursive: true });
|
|
2254
2728
|
execFileSync("tar", ["-xzf", archivePath, "-C", targetDir], { stdio: "inherit" });
|
|
2255
2729
|
return targetDir;
|
|
@@ -2276,21 +2750,21 @@ function findSpecPath(explicitPath, modelPath) {
|
|
|
2276
2750
|
const candidates = explicitPath ? [explicitPath] : [join2(process.cwd(), DEFAULT_SPEC_FILE), join2(modelPath, DEFAULT_SPEC_FILE)];
|
|
2277
2751
|
for (const candidate of candidates) {
|
|
2278
2752
|
const resolved = resolve2(candidate);
|
|
2279
|
-
if (
|
|
2753
|
+
if (existsSync5(resolved) && statSync4(resolved).isFile()) return resolved;
|
|
2280
2754
|
}
|
|
2281
2755
|
return null;
|
|
2282
2756
|
}
|
|
2283
2757
|
function loadSystemPromptFromSpec(specPath) {
|
|
2284
|
-
const spec = JSON.parse(
|
|
2758
|
+
const spec = JSON.parse(readFileSync7(specPath, "utf8"));
|
|
2285
2759
|
return buildSystemPrompt(spec);
|
|
2286
2760
|
}
|
|
2287
2761
|
function loadJsonSchemaForServe(schemaPath) {
|
|
2288
2762
|
const resolved = resolve2(schemaPath);
|
|
2289
|
-
if (!
|
|
2763
|
+
if (!existsSync5(resolved) || !statSync4(resolved).isFile()) {
|
|
2290
2764
|
throw new Error(`JSON schema file not found: ${schemaPath}`);
|
|
2291
2765
|
}
|
|
2292
2766
|
try {
|
|
2293
|
-
const schema = JSON.parse(
|
|
2767
|
+
const schema = JSON.parse(readFileSync7(resolved, "utf8"));
|
|
2294
2768
|
return JSON.stringify(schema);
|
|
2295
2769
|
} catch (err) {
|
|
2296
2770
|
throw new Error(`JSON schema file is not valid JSON: ${schemaPath}`);
|
|
@@ -2304,9 +2778,9 @@ function writeReferenceServerScript(cacheDir) {
|
|
|
2304
2778
|
return scriptPath;
|
|
2305
2779
|
}
|
|
2306
2780
|
async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
|
|
2307
|
-
if (
|
|
2781
|
+
if (existsSync5(target)) {
|
|
2308
2782
|
const resolved = resolve2(target);
|
|
2309
|
-
const stats =
|
|
2783
|
+
const stats = statSync4(resolved);
|
|
2310
2784
|
if (stats.isDirectory()) {
|
|
2311
2785
|
return {
|
|
2312
2786
|
modelPath: resolved,
|
|
@@ -2330,7 +2804,7 @@ async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
|
|
|
2330
2804
|
const modelCacheDir = join2(cacheDir, fullId);
|
|
2331
2805
|
const archivePath = join2(modelCacheDir, data.filename);
|
|
2332
2806
|
const extractDir = join2(modelCacheDir, "model");
|
|
2333
|
-
if (!
|
|
2807
|
+
if (!existsSync5(archivePath) || forceDownload) {
|
|
2334
2808
|
await downloadUrlToFile(data.url, archivePath);
|
|
2335
2809
|
}
|
|
2336
2810
|
extractArchive(archivePath, extractDir, forceDownload);
|
|
@@ -2434,7 +2908,7 @@ function ollamaSlug(name) {
|
|
|
2434
2908
|
}
|
|
2435
2909
|
function firstExisting(candidates) {
|
|
2436
2910
|
for (const candidate of candidates) {
|
|
2437
|
-
if (candidate &&
|
|
2911
|
+
if (candidate && existsSync5(candidate) && statSync4(candidate).isFile()) {
|
|
2438
2912
|
return resolve2(candidate);
|
|
2439
2913
|
}
|
|
2440
2914
|
}
|
|
@@ -2446,7 +2920,7 @@ function llamaCppDir(explicit) {
|
|
|
2446
2920
|
function resolveConvertScript(opts, required) {
|
|
2447
2921
|
if (opts.convertScript) {
|
|
2448
2922
|
const resolved = resolve2(opts.convertScript);
|
|
2449
|
-
if (required && !
|
|
2923
|
+
if (required && !existsSync5(resolved)) {
|
|
2450
2924
|
throw new Error(`Conversion script not found: ${opts.convertScript}`);
|
|
2451
2925
|
}
|
|
2452
2926
|
return resolved;
|
|
@@ -2465,7 +2939,7 @@ function resolveConvertScript(opts, required) {
|
|
|
2465
2939
|
function resolveQuantizeBin(opts, required) {
|
|
2466
2940
|
if (opts.quantizeBin) {
|
|
2467
2941
|
const resolved = resolve2(opts.quantizeBin);
|
|
2468
|
-
if (required && !
|
|
2942
|
+
if (required && !existsSync5(resolved)) {
|
|
2469
2943
|
throw new Error(`llama-quantize binary not found: ${opts.quantizeBin}`);
|
|
2470
2944
|
}
|
|
2471
2945
|
return resolved;
|
|
@@ -2600,7 +3074,7 @@ function registerModelsCommands(parent) {
|
|
|
2600
3074
|
opts
|
|
2601
3075
|
);
|
|
2602
3076
|
const outputPath = resolveOutputPath(cmdOpts.output, data.filename);
|
|
2603
|
-
if (
|
|
3077
|
+
if (existsSync5(outputPath) && !cmdOpts.force) {
|
|
2604
3078
|
throw new Error(`Output file already exists: ${outputPath}. Use --force to overwrite.`);
|
|
2605
3079
|
}
|
|
2606
3080
|
const bytes = await downloadUrlToFile(data.url, outputPath);
|
|
@@ -2709,7 +3183,7 @@ function registerModelsCommands(parent) {
|
|
|
2709
3183
|
ollama: ollamaInfo
|
|
2710
3184
|
};
|
|
2711
3185
|
if (printOnly) return printExportPlan(plan);
|
|
2712
|
-
if (
|
|
3186
|
+
if (existsSync5(finalPath) && !cmdOpts.force) {
|
|
2713
3187
|
throw new Error(`Output already exists: ${finalPath}. Use --force to overwrite.`);
|
|
2714
3188
|
}
|
|
2715
3189
|
mkdirSync3(outputDir, { recursive: true });
|
|
@@ -2730,7 +3204,7 @@ function registerModelsCommands(parent) {
|
|
|
2730
3204
|
finalPath,
|
|
2731
3205
|
quantPlan.quantizeType
|
|
2732
3206
|
]);
|
|
2733
|
-
if (!cmdOpts.keepIntermediate && intermediatePath &&
|
|
3207
|
+
if (!cmdOpts.keepIntermediate && intermediatePath && existsSync5(intermediatePath)) {
|
|
2734
3208
|
rmSync(intermediatePath, { force: true });
|
|
2735
3209
|
}
|
|
2736
3210
|
}
|
|
@@ -2783,10 +3257,10 @@ function registerModelsCommands(parent) {
|
|
|
2783
3257
|
]);
|
|
2784
3258
|
return;
|
|
2785
3259
|
}
|
|
2786
|
-
if (
|
|
3260
|
+
if (existsSync5(venvDir) && cmdOpts.force) {
|
|
2787
3261
|
rmSync(venvDir, { recursive: true, force: true });
|
|
2788
3262
|
}
|
|
2789
|
-
if (!
|
|
3263
|
+
if (!existsSync5(venvPython)) {
|
|
2790
3264
|
mkdirSync3(cacheDir, { recursive: true });
|
|
2791
3265
|
execFileSync(commands[0][0], commands[0].slice(1), { stdio: "inherit" });
|
|
2792
3266
|
}
|
|
@@ -2927,15 +3401,15 @@ var KIND_LABELS = {
|
|
|
2927
3401
|
refund: "Refund",
|
|
2928
3402
|
adjustment: "Adjustment"
|
|
2929
3403
|
};
|
|
2930
|
-
function
|
|
3404
|
+
function formatCents3(cents) {
|
|
2931
3405
|
const sign = cents < 0 ? "-" : "";
|
|
2932
3406
|
const abs = Math.abs(cents);
|
|
2933
3407
|
return `${sign}$${(abs / 100).toFixed(2)}`;
|
|
2934
3408
|
}
|
|
2935
3409
|
function formatSigned(cents) {
|
|
2936
|
-
if (cents > 0) return chalk3.green(`+${
|
|
2937
|
-
if (cents < 0) return chalk3.red(
|
|
2938
|
-
return
|
|
3410
|
+
if (cents > 0) return chalk3.green(`+${formatCents3(cents)}`);
|
|
3411
|
+
if (cents < 0) return chalk3.red(formatCents3(cents));
|
|
3412
|
+
return formatCents3(cents);
|
|
2939
3413
|
}
|
|
2940
3414
|
function registerBalanceCommands(parent) {
|
|
2941
3415
|
parent.command("balance").description("Show credit balance and recent transactions").option("-n, --limit <n>", "Number of transactions to show (default 10)", "10").action(async (options) => {
|
|
@@ -2949,10 +3423,10 @@ function registerBalanceCommands(parent) {
|
|
|
2949
3423
|
return printJson({ balance, transactions });
|
|
2950
3424
|
}
|
|
2951
3425
|
const lowBalance = balance.balance_cents < 100;
|
|
2952
|
-
const balanceLine = lowBalance ? chalk3.red(
|
|
3426
|
+
const balanceLine = lowBalance ? chalk3.red(formatCents3(balance.balance_cents)) + chalk3.dim(" (low)") : chalk3.bold.green(formatCents3(balance.balance_cents));
|
|
2953
3427
|
printDetail([
|
|
2954
3428
|
["Credits", balanceLine],
|
|
2955
|
-
["Lifetime top-ups",
|
|
3429
|
+
["Lifetime top-ups", formatCents3(balance.lifetime_topup_cents)]
|
|
2956
3430
|
]);
|
|
2957
3431
|
if (transactions && transactions.length > 0) {
|
|
2958
3432
|
console.log(`
|
|
@@ -2963,7 +3437,7 @@ ${chalk3.bold("Recent transactions")}`);
|
|
|
2963
3437
|
formatDate(row.created_at),
|
|
2964
3438
|
KIND_LABELS[row.kind] ?? row.kind,
|
|
2965
3439
|
formatSigned(row.amount_cents),
|
|
2966
|
-
|
|
3440
|
+
formatCents3(row.balance_after_cents),
|
|
2967
3441
|
row.reference_id ? shortId(row.reference_id) : chalk3.dim("\u2014")
|
|
2968
3442
|
])
|
|
2969
3443
|
);
|
|
@@ -3261,7 +3735,7 @@ function printValidation(checks) {
|
|
|
3261
3735
|
}
|
|
3262
3736
|
|
|
3263
3737
|
// src/commands/push.ts
|
|
3264
|
-
import { readFileSync as
|
|
3738
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
3265
3739
|
import { resolve as resolve3 } from "path";
|
|
3266
3740
|
function registerPushCommand(parent) {
|
|
3267
3741
|
parent.command("push").description("Push local spec to the Tuned Tensor API").option("-f, --file <path>", "Spec file path", DEFAULT_SPEC_FILE).action(async (cmdOpts) => {
|
|
@@ -3281,7 +3755,7 @@ function registerPushCommand(parent) {
|
|
|
3281
3755
|
} else {
|
|
3282
3756
|
const res = await post("/behavior-specs", body, opts);
|
|
3283
3757
|
data = res.data;
|
|
3284
|
-
const raw = JSON.parse(
|
|
3758
|
+
const raw = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
3285
3759
|
raw.id = data.id;
|
|
3286
3760
|
writeFileSync4(filePath, JSON.stringify(raw, null, 2) + "\n");
|
|
3287
3761
|
}
|
|
@@ -3294,7 +3768,7 @@ function registerPushCommand(parent) {
|
|
|
3294
3768
|
|
|
3295
3769
|
// src/index.ts
|
|
3296
3770
|
var program = new Command();
|
|
3297
|
-
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.
|
|
3771
|
+
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.20").option("-k, --api-key <key>", "API key (overrides stored key)").option(
|
|
3298
3772
|
"-u, --base-url <url>",
|
|
3299
3773
|
"API base URL (default: https://tunedtensor.com)"
|
|
3300
3774
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|
|
@@ -3308,6 +3782,7 @@ registerAuthCommands(program);
|
|
|
3308
3782
|
registerSpecsCommands(program);
|
|
3309
3783
|
registerRunsCommands(program);
|
|
3310
3784
|
registerDatasetsCommands(program);
|
|
3785
|
+
registerLabelCommands(program);
|
|
3311
3786
|
registerModelsCommands(program);
|
|
3312
3787
|
registerBalanceCommands(program);
|
|
3313
3788
|
registerTopupCommands(program);
|