@tuned-tensor/cli 0.4.19 → 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 +14 -0
- package/dist/index.js +471 -42
- 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 = [
|
|
@@ -1153,27 +1166,425 @@ function registerDatasetsCommands(parent) {
|
|
|
1153
1166
|
});
|
|
1154
1167
|
}
|
|
1155
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
|
+
|
|
1156
1567
|
// src/commands/models.ts
|
|
1157
1568
|
import { spawn as spawn2, execFileSync } from "child_process";
|
|
1158
1569
|
import { createHash } from "crypto";
|
|
1159
1570
|
import {
|
|
1160
|
-
existsSync as
|
|
1571
|
+
existsSync as existsSync5,
|
|
1161
1572
|
mkdirSync as mkdirSync3,
|
|
1162
|
-
statSync as
|
|
1573
|
+
statSync as statSync4,
|
|
1163
1574
|
createWriteStream,
|
|
1164
1575
|
unlinkSync,
|
|
1165
|
-
readFileSync as
|
|
1576
|
+
readFileSync as readFileSync7,
|
|
1166
1577
|
writeFileSync as writeFileSync3,
|
|
1167
1578
|
readdirSync,
|
|
1168
1579
|
rmSync
|
|
1169
1580
|
} from "fs";
|
|
1170
1581
|
import { homedir as homedir2 } from "os";
|
|
1171
|
-
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";
|
|
1172
1583
|
import { Readable, Transform } from "stream";
|
|
1173
1584
|
import { pipeline } from "stream/promises";
|
|
1174
1585
|
|
|
1175
1586
|
// src/commands/init.ts
|
|
1176
|
-
import { existsSync as
|
|
1587
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
|
|
1177
1588
|
import { resolve } from "path";
|
|
1178
1589
|
var DEFAULT_SPEC_FILE = "tunedtensor.json";
|
|
1179
1590
|
var SCAFFOLD = {
|
|
@@ -1191,7 +1602,7 @@ var SCAFFOLD = {
|
|
|
1191
1602
|
function registerInitCommand(parent) {
|
|
1192
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) => {
|
|
1193
1604
|
const filePath = resolve(cmdOpts.file);
|
|
1194
|
-
if (
|
|
1605
|
+
if (existsSync4(filePath)) {
|
|
1195
1606
|
printWarning(`${cmdOpts.file} already exists. Use tt eval to validate it or edit it directly.`);
|
|
1196
1607
|
return;
|
|
1197
1608
|
}
|
|
@@ -1208,14 +1619,14 @@ function registerInitCommand(parent) {
|
|
|
1208
1619
|
}
|
|
1209
1620
|
function loadSpec(filePath) {
|
|
1210
1621
|
const resolved = resolve(filePath || DEFAULT_SPEC_FILE);
|
|
1211
|
-
if (!
|
|
1622
|
+
if (!existsSync4(resolved)) {
|
|
1212
1623
|
printError(
|
|
1213
1624
|
`Spec file not found: ${filePath || DEFAULT_SPEC_FILE}
|
|
1214
1625
|
Run \`tt init\` to create one.`
|
|
1215
1626
|
);
|
|
1216
1627
|
process.exit(1);
|
|
1217
1628
|
}
|
|
1218
|
-
const raw = JSON.parse(
|
|
1629
|
+
const raw = JSON.parse(readFileSync6(resolved, "utf-8"));
|
|
1219
1630
|
return raw;
|
|
1220
1631
|
}
|
|
1221
1632
|
|
|
@@ -1230,7 +1641,7 @@ var COMPLETION_ROUTES = /* @__PURE__ */ new Set(["/v1/chat/completions", "/chat/
|
|
|
1230
1641
|
function parsePositiveInteger(value, fallback) {
|
|
1231
1642
|
return Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
1232
1643
|
}
|
|
1233
|
-
function
|
|
1644
|
+
function sleep2(ms) {
|
|
1234
1645
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1235
1646
|
}
|
|
1236
1647
|
function readRequestBody(req) {
|
|
@@ -1552,7 +1963,7 @@ var ManagedModelServer = class {
|
|
|
1552
1963
|
child.kill("SIGTERM");
|
|
1553
1964
|
await Promise.race([
|
|
1554
1965
|
new Promise((resolve4) => child.once("exit", () => resolve4())),
|
|
1555
|
-
|
|
1966
|
+
sleep2(5e3).then(() => {
|
|
1556
1967
|
if (child.exitCode == null && child.signalCode == null) child.kill("SIGKILL");
|
|
1557
1968
|
})
|
|
1558
1969
|
]);
|
|
@@ -1582,7 +1993,7 @@ var ManagedModelServer = class {
|
|
|
1582
1993
|
const health = await this.checkHealth();
|
|
1583
1994
|
if (health.ok) return;
|
|
1584
1995
|
if (!this.child) break;
|
|
1585
|
-
await
|
|
1996
|
+
await sleep2(50);
|
|
1586
1997
|
}
|
|
1587
1998
|
throw new Error("Model server did not become healthy within 120 seconds");
|
|
1588
1999
|
}
|
|
@@ -2091,7 +2502,7 @@ if __name__ == "__main__":
|
|
|
2091
2502
|
`;
|
|
2092
2503
|
function resolveOutputPath(output, filename) {
|
|
2093
2504
|
if (!output) return filename;
|
|
2094
|
-
if (
|
|
2505
|
+
if (existsSync5(output) && statSync4(output).isDirectory()) {
|
|
2095
2506
|
return join2(output, filename);
|
|
2096
2507
|
}
|
|
2097
2508
|
return output;
|
|
@@ -2212,7 +2623,7 @@ function runtimePythonPath(cacheDir) {
|
|
|
2212
2623
|
function resolveServePython(cacheDir, explicitPython) {
|
|
2213
2624
|
if (explicitPython) return explicitPython;
|
|
2214
2625
|
const managedPython = runtimePythonPath(cacheDir);
|
|
2215
|
-
return
|
|
2626
|
+
return existsSync5(managedPython) ? managedPython : "python3";
|
|
2216
2627
|
}
|
|
2217
2628
|
function commandExists(command) {
|
|
2218
2629
|
try {
|
|
@@ -2291,11 +2702,28 @@ function safeSegment(value) {
|
|
|
2291
2702
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "model";
|
|
2292
2703
|
}
|
|
2293
2704
|
function directoryHasFiles(path) {
|
|
2294
|
-
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
|
+
}
|
|
2295
2722
|
}
|
|
2296
2723
|
function extractArchive(archivePath, targetDir, force) {
|
|
2297
2724
|
if (directoryHasFiles(targetDir) && !force) return targetDir;
|
|
2298
|
-
|
|
2725
|
+
validateArchiveEntries(archivePath);
|
|
2726
|
+
if (existsSync5(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
2299
2727
|
mkdirSync3(targetDir, { recursive: true });
|
|
2300
2728
|
execFileSync("tar", ["-xzf", archivePath, "-C", targetDir], { stdio: "inherit" });
|
|
2301
2729
|
return targetDir;
|
|
@@ -2322,21 +2750,21 @@ function findSpecPath(explicitPath, modelPath) {
|
|
|
2322
2750
|
const candidates = explicitPath ? [explicitPath] : [join2(process.cwd(), DEFAULT_SPEC_FILE), join2(modelPath, DEFAULT_SPEC_FILE)];
|
|
2323
2751
|
for (const candidate of candidates) {
|
|
2324
2752
|
const resolved = resolve2(candidate);
|
|
2325
|
-
if (
|
|
2753
|
+
if (existsSync5(resolved) && statSync4(resolved).isFile()) return resolved;
|
|
2326
2754
|
}
|
|
2327
2755
|
return null;
|
|
2328
2756
|
}
|
|
2329
2757
|
function loadSystemPromptFromSpec(specPath) {
|
|
2330
|
-
const spec = JSON.parse(
|
|
2758
|
+
const spec = JSON.parse(readFileSync7(specPath, "utf8"));
|
|
2331
2759
|
return buildSystemPrompt(spec);
|
|
2332
2760
|
}
|
|
2333
2761
|
function loadJsonSchemaForServe(schemaPath) {
|
|
2334
2762
|
const resolved = resolve2(schemaPath);
|
|
2335
|
-
if (!
|
|
2763
|
+
if (!existsSync5(resolved) || !statSync4(resolved).isFile()) {
|
|
2336
2764
|
throw new Error(`JSON schema file not found: ${schemaPath}`);
|
|
2337
2765
|
}
|
|
2338
2766
|
try {
|
|
2339
|
-
const schema = JSON.parse(
|
|
2767
|
+
const schema = JSON.parse(readFileSync7(resolved, "utf8"));
|
|
2340
2768
|
return JSON.stringify(schema);
|
|
2341
2769
|
} catch (err) {
|
|
2342
2770
|
throw new Error(`JSON schema file is not valid JSON: ${schemaPath}`);
|
|
@@ -2350,9 +2778,9 @@ function writeReferenceServerScript(cacheDir) {
|
|
|
2350
2778
|
return scriptPath;
|
|
2351
2779
|
}
|
|
2352
2780
|
async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
|
|
2353
|
-
if (
|
|
2781
|
+
if (existsSync5(target)) {
|
|
2354
2782
|
const resolved = resolve2(target);
|
|
2355
|
-
const stats =
|
|
2783
|
+
const stats = statSync4(resolved);
|
|
2356
2784
|
if (stats.isDirectory()) {
|
|
2357
2785
|
return {
|
|
2358
2786
|
modelPath: resolved,
|
|
@@ -2376,7 +2804,7 @@ async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
|
|
|
2376
2804
|
const modelCacheDir = join2(cacheDir, fullId);
|
|
2377
2805
|
const archivePath = join2(modelCacheDir, data.filename);
|
|
2378
2806
|
const extractDir = join2(modelCacheDir, "model");
|
|
2379
|
-
if (!
|
|
2807
|
+
if (!existsSync5(archivePath) || forceDownload) {
|
|
2380
2808
|
await downloadUrlToFile(data.url, archivePath);
|
|
2381
2809
|
}
|
|
2382
2810
|
extractArchive(archivePath, extractDir, forceDownload);
|
|
@@ -2480,7 +2908,7 @@ function ollamaSlug(name) {
|
|
|
2480
2908
|
}
|
|
2481
2909
|
function firstExisting(candidates) {
|
|
2482
2910
|
for (const candidate of candidates) {
|
|
2483
|
-
if (candidate &&
|
|
2911
|
+
if (candidate && existsSync5(candidate) && statSync4(candidate).isFile()) {
|
|
2484
2912
|
return resolve2(candidate);
|
|
2485
2913
|
}
|
|
2486
2914
|
}
|
|
@@ -2492,7 +2920,7 @@ function llamaCppDir(explicit) {
|
|
|
2492
2920
|
function resolveConvertScript(opts, required) {
|
|
2493
2921
|
if (opts.convertScript) {
|
|
2494
2922
|
const resolved = resolve2(opts.convertScript);
|
|
2495
|
-
if (required && !
|
|
2923
|
+
if (required && !existsSync5(resolved)) {
|
|
2496
2924
|
throw new Error(`Conversion script not found: ${opts.convertScript}`);
|
|
2497
2925
|
}
|
|
2498
2926
|
return resolved;
|
|
@@ -2511,7 +2939,7 @@ function resolveConvertScript(opts, required) {
|
|
|
2511
2939
|
function resolveQuantizeBin(opts, required) {
|
|
2512
2940
|
if (opts.quantizeBin) {
|
|
2513
2941
|
const resolved = resolve2(opts.quantizeBin);
|
|
2514
|
-
if (required && !
|
|
2942
|
+
if (required && !existsSync5(resolved)) {
|
|
2515
2943
|
throw new Error(`llama-quantize binary not found: ${opts.quantizeBin}`);
|
|
2516
2944
|
}
|
|
2517
2945
|
return resolved;
|
|
@@ -2646,7 +3074,7 @@ function registerModelsCommands(parent) {
|
|
|
2646
3074
|
opts
|
|
2647
3075
|
);
|
|
2648
3076
|
const outputPath = resolveOutputPath(cmdOpts.output, data.filename);
|
|
2649
|
-
if (
|
|
3077
|
+
if (existsSync5(outputPath) && !cmdOpts.force) {
|
|
2650
3078
|
throw new Error(`Output file already exists: ${outputPath}. Use --force to overwrite.`);
|
|
2651
3079
|
}
|
|
2652
3080
|
const bytes = await downloadUrlToFile(data.url, outputPath);
|
|
@@ -2755,7 +3183,7 @@ function registerModelsCommands(parent) {
|
|
|
2755
3183
|
ollama: ollamaInfo
|
|
2756
3184
|
};
|
|
2757
3185
|
if (printOnly) return printExportPlan(plan);
|
|
2758
|
-
if (
|
|
3186
|
+
if (existsSync5(finalPath) && !cmdOpts.force) {
|
|
2759
3187
|
throw new Error(`Output already exists: ${finalPath}. Use --force to overwrite.`);
|
|
2760
3188
|
}
|
|
2761
3189
|
mkdirSync3(outputDir, { recursive: true });
|
|
@@ -2776,7 +3204,7 @@ function registerModelsCommands(parent) {
|
|
|
2776
3204
|
finalPath,
|
|
2777
3205
|
quantPlan.quantizeType
|
|
2778
3206
|
]);
|
|
2779
|
-
if (!cmdOpts.keepIntermediate && intermediatePath &&
|
|
3207
|
+
if (!cmdOpts.keepIntermediate && intermediatePath && existsSync5(intermediatePath)) {
|
|
2780
3208
|
rmSync(intermediatePath, { force: true });
|
|
2781
3209
|
}
|
|
2782
3210
|
}
|
|
@@ -2829,10 +3257,10 @@ function registerModelsCommands(parent) {
|
|
|
2829
3257
|
]);
|
|
2830
3258
|
return;
|
|
2831
3259
|
}
|
|
2832
|
-
if (
|
|
3260
|
+
if (existsSync5(venvDir) && cmdOpts.force) {
|
|
2833
3261
|
rmSync(venvDir, { recursive: true, force: true });
|
|
2834
3262
|
}
|
|
2835
|
-
if (!
|
|
3263
|
+
if (!existsSync5(venvPython)) {
|
|
2836
3264
|
mkdirSync3(cacheDir, { recursive: true });
|
|
2837
3265
|
execFileSync(commands[0][0], commands[0].slice(1), { stdio: "inherit" });
|
|
2838
3266
|
}
|
|
@@ -2973,15 +3401,15 @@ var KIND_LABELS = {
|
|
|
2973
3401
|
refund: "Refund",
|
|
2974
3402
|
adjustment: "Adjustment"
|
|
2975
3403
|
};
|
|
2976
|
-
function
|
|
3404
|
+
function formatCents3(cents) {
|
|
2977
3405
|
const sign = cents < 0 ? "-" : "";
|
|
2978
3406
|
const abs = Math.abs(cents);
|
|
2979
3407
|
return `${sign}$${(abs / 100).toFixed(2)}`;
|
|
2980
3408
|
}
|
|
2981
3409
|
function formatSigned(cents) {
|
|
2982
|
-
if (cents > 0) return chalk3.green(`+${
|
|
2983
|
-
if (cents < 0) return chalk3.red(
|
|
2984
|
-
return
|
|
3410
|
+
if (cents > 0) return chalk3.green(`+${formatCents3(cents)}`);
|
|
3411
|
+
if (cents < 0) return chalk3.red(formatCents3(cents));
|
|
3412
|
+
return formatCents3(cents);
|
|
2985
3413
|
}
|
|
2986
3414
|
function registerBalanceCommands(parent) {
|
|
2987
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) => {
|
|
@@ -2995,10 +3423,10 @@ function registerBalanceCommands(parent) {
|
|
|
2995
3423
|
return printJson({ balance, transactions });
|
|
2996
3424
|
}
|
|
2997
3425
|
const lowBalance = balance.balance_cents < 100;
|
|
2998
|
-
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));
|
|
2999
3427
|
printDetail([
|
|
3000
3428
|
["Credits", balanceLine],
|
|
3001
|
-
["Lifetime top-ups",
|
|
3429
|
+
["Lifetime top-ups", formatCents3(balance.lifetime_topup_cents)]
|
|
3002
3430
|
]);
|
|
3003
3431
|
if (transactions && transactions.length > 0) {
|
|
3004
3432
|
console.log(`
|
|
@@ -3009,7 +3437,7 @@ ${chalk3.bold("Recent transactions")}`);
|
|
|
3009
3437
|
formatDate(row.created_at),
|
|
3010
3438
|
KIND_LABELS[row.kind] ?? row.kind,
|
|
3011
3439
|
formatSigned(row.amount_cents),
|
|
3012
|
-
|
|
3440
|
+
formatCents3(row.balance_after_cents),
|
|
3013
3441
|
row.reference_id ? shortId(row.reference_id) : chalk3.dim("\u2014")
|
|
3014
3442
|
])
|
|
3015
3443
|
);
|
|
@@ -3307,7 +3735,7 @@ function printValidation(checks) {
|
|
|
3307
3735
|
}
|
|
3308
3736
|
|
|
3309
3737
|
// src/commands/push.ts
|
|
3310
|
-
import { readFileSync as
|
|
3738
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
3311
3739
|
import { resolve as resolve3 } from "path";
|
|
3312
3740
|
function registerPushCommand(parent) {
|
|
3313
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) => {
|
|
@@ -3327,7 +3755,7 @@ function registerPushCommand(parent) {
|
|
|
3327
3755
|
} else {
|
|
3328
3756
|
const res = await post("/behavior-specs", body, opts);
|
|
3329
3757
|
data = res.data;
|
|
3330
|
-
const raw = JSON.parse(
|
|
3758
|
+
const raw = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
3331
3759
|
raw.id = data.id;
|
|
3332
3760
|
writeFileSync4(filePath, JSON.stringify(raw, null, 2) + "\n");
|
|
3333
3761
|
}
|
|
@@ -3340,7 +3768,7 @@ function registerPushCommand(parent) {
|
|
|
3340
3768
|
|
|
3341
3769
|
// src/index.ts
|
|
3342
3770
|
var program = new Command();
|
|
3343
|
-
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(
|
|
3344
3772
|
"-u, --base-url <url>",
|
|
3345
3773
|
"API base URL (default: https://tunedtensor.com)"
|
|
3346
3774
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|
|
@@ -3354,6 +3782,7 @@ registerAuthCommands(program);
|
|
|
3354
3782
|
registerSpecsCommands(program);
|
|
3355
3783
|
registerRunsCommands(program);
|
|
3356
3784
|
registerDatasetsCommands(program);
|
|
3785
|
+
registerLabelCommands(program);
|
|
3357
3786
|
registerModelsCommands(program);
|
|
3358
3787
|
registerBalanceCommands(program);
|
|
3359
3788
|
registerTopupCommands(program);
|