@tuned-tensor/cli 0.4.19 → 0.4.21
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 +687 -63
- 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,12 +371,23 @@ 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 = [
|
|
374
387
|
"google/gemma-4-E2B-it",
|
|
375
388
|
"google/gemma-4-E4B-it",
|
|
376
389
|
"Qwen/Qwen3.5-2B",
|
|
390
|
+
"Qwen/Qwen3-VL-2B-Instruct",
|
|
377
391
|
"Qwen/Qwen3.5-4B",
|
|
378
392
|
"meta-llama/Llama-3.2-3B-Instruct",
|
|
379
393
|
"microsoft/Phi-4-mini-instruct",
|
|
@@ -394,6 +408,9 @@ for (const [alias, model] of [
|
|
|
394
408
|
["qwen/qwen3.5-2b", "Qwen/Qwen3.5-2B"],
|
|
395
409
|
["Qwen/Qwen3.5-2B-Base", "Qwen/Qwen3.5-2B"],
|
|
396
410
|
["qwen/qwen3.5-2b-base", "Qwen/Qwen3.5-2B"],
|
|
411
|
+
["qwen/qwen3-vl-2b-instruct", "Qwen/Qwen3-VL-2B-Instruct"],
|
|
412
|
+
["Qwen/Qwen3-VL-2B", "Qwen/Qwen3-VL-2B-Instruct"],
|
|
413
|
+
["qwen/qwen3-vl-2b", "Qwen/Qwen3-VL-2B-Instruct"],
|
|
397
414
|
["qwen/qwen3.5-4b", "Qwen/Qwen3.5-4B"],
|
|
398
415
|
["Qwen/Qwen3.5-4B-Base", "Qwen/Qwen3.5-4B"],
|
|
399
416
|
["qwen/qwen3.5-4b-base", "Qwen/Qwen3.5-4B"],
|
|
@@ -946,6 +963,7 @@ Eval Results (${data._evals.length}):`);
|
|
|
946
963
|
|
|
947
964
|
// src/commands/datasets.ts
|
|
948
965
|
import { existsSync as existsSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
966
|
+
var DOCUMENT_OCR_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
949
967
|
function formatBytes(bytes) {
|
|
950
968
|
if (bytes < 1024) return `${bytes} B`;
|
|
951
969
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -999,10 +1017,85 @@ function findControlChar(value) {
|
|
|
999
1017
|
function formatCodepoint(code) {
|
|
1000
1018
|
return `U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
1001
1019
|
}
|
|
1002
|
-
function
|
|
1020
|
+
function detectRowFormat(record) {
|
|
1021
|
+
const input = record.input;
|
|
1022
|
+
if (input && typeof input === "object" && typeof input.prompt === "string" && Array.isArray(input.assets) && input.assets.length > 0 && typeof record.output === "string") {
|
|
1023
|
+
return "document_ocr_jsonl";
|
|
1024
|
+
}
|
|
1025
|
+
if (typeof record.input === "string" || typeof record.output === "string" || !("input" in record) || !("output" in record)) {
|
|
1026
|
+
return "jsonl";
|
|
1027
|
+
}
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
function validateDocumentOcrRow(record, rowNumber, errors) {
|
|
1031
|
+
const input = record.input;
|
|
1032
|
+
const assets = input.assets;
|
|
1033
|
+
if (typeof input.prompt !== "string" || input.prompt.trim().length === 0) {
|
|
1034
|
+
errors.push(`Row ${rowNumber}: OCR input.prompt must be a non-empty string`);
|
|
1035
|
+
} else {
|
|
1036
|
+
const hit = findControlChar(input.prompt);
|
|
1037
|
+
if (hit) {
|
|
1038
|
+
errors.push(
|
|
1039
|
+
`Row ${rowNumber}: OCR input.prompt contains invisible control character ${formatCodepoint(hit.codepoint)} (${hit.name}) at offset ${hit.offset} \u2014 strip or escape before uploading; some training paths split on it`
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (typeof record.output !== "string" || record.output.trim().length === 0) {
|
|
1044
|
+
errors.push(`Row ${rowNumber}: OCR output must be a non-empty string`);
|
|
1045
|
+
} else {
|
|
1046
|
+
const hit = findControlChar(record.output);
|
|
1047
|
+
if (hit) {
|
|
1048
|
+
errors.push(
|
|
1049
|
+
`Row ${rowNumber}: OCR output contains invisible control character ${formatCodepoint(hit.codepoint)} (${hit.name}) at offset ${hit.offset} \u2014 strip or escape before uploading; some training paths split on it`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (assets.length > 8) {
|
|
1054
|
+
errors.push(`Row ${rowNumber}: OCR rows support at most 8 image assets`);
|
|
1055
|
+
}
|
|
1056
|
+
for (const [assetIndex, asset] of assets.entries()) {
|
|
1057
|
+
if (!asset || typeof asset !== "object" || Array.isArray(asset)) {
|
|
1058
|
+
errors.push(`Row ${rowNumber}: asset ${assetIndex + 1} must be an object`);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
const a = asset;
|
|
1062
|
+
if (a.type !== void 0 && a.type !== "image") {
|
|
1063
|
+
errors.push(`Row ${rowNumber}: asset ${assetIndex + 1} type must be "image"`);
|
|
1064
|
+
}
|
|
1065
|
+
if (a.mime_type !== void 0 && (typeof a.mime_type !== "string" || !DOCUMENT_OCR_MIME_TYPES.has(a.mime_type))) {
|
|
1066
|
+
errors.push(
|
|
1067
|
+
`Row ${rowNumber}: asset ${assetIndex + 1} mime_type must be one of image/png, image/jpeg, image/webp`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
if (a.page !== void 0 && (typeof a.page !== "number" || !Number.isInteger(a.page) || a.page < 1)) {
|
|
1071
|
+
errors.push(`Row ${rowNumber}: asset ${assetIndex + 1} page must be a positive integer`);
|
|
1072
|
+
}
|
|
1073
|
+
const hasReference = typeof a.data_uri === "string" && a.data_uri.trim().length > 0 || typeof a.uri === "string" && a.uri.trim().length > 0 || typeof a.path === "string" && a.path.trim().length > 0;
|
|
1074
|
+
if (!hasReference) {
|
|
1075
|
+
errors.push(`Row ${rowNumber}: asset ${assetIndex + 1} must include data_uri, uri, or path`);
|
|
1076
|
+
}
|
|
1077
|
+
if (typeof a.data_uri === "string") {
|
|
1078
|
+
const match = /^data:(image\/(?:png|jpeg|webp));base64,[A-Za-z0-9+/=\s]+$/.exec(a.data_uri);
|
|
1079
|
+
if (!match) {
|
|
1080
|
+
errors.push(
|
|
1081
|
+
`Row ${rowNumber}: asset ${assetIndex + 1} data_uri must be a base64 image/png, image/jpeg, or image/webp data URI`
|
|
1082
|
+
);
|
|
1083
|
+
} else if (typeof a.mime_type === "string" && a.mime_type !== match[1]) {
|
|
1084
|
+
errors.push(`Row ${rowNumber}: asset ${assetIndex + 1} mime_type does not match data_uri media type`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function parseDatasetFormat(value) {
|
|
1090
|
+
if (!value) return void 0;
|
|
1091
|
+
if (value === "jsonl" || value === "document_ocr_jsonl") return value;
|
|
1092
|
+
throw new Error("--format must be one of: jsonl, document_ocr_jsonl");
|
|
1093
|
+
}
|
|
1094
|
+
function validateDatasetFile(file, requestedFormat) {
|
|
1003
1095
|
const lines = readFileSync4(file, "utf8").split(/\r?\n/);
|
|
1004
1096
|
const errors = [];
|
|
1005
1097
|
let rowCount = 0;
|
|
1098
|
+
let detectedFormat = null;
|
|
1006
1099
|
for (const [index, rawLine] of lines.entries()) {
|
|
1007
1100
|
const line = rawLine.trim();
|
|
1008
1101
|
if (!line) continue;
|
|
@@ -1026,6 +1119,26 @@ function validateDatasetFile(file) {
|
|
|
1026
1119
|
);
|
|
1027
1120
|
continue;
|
|
1028
1121
|
}
|
|
1122
|
+
const rowFormat = detectRowFormat(record);
|
|
1123
|
+
if (!rowFormat) {
|
|
1124
|
+
errors.push(
|
|
1125
|
+
`Row ${rowNumber}: expected text {"input": string, "output": string} or OCR {"input":{"prompt": string, "assets": [...]}, "output": string}`
|
|
1126
|
+
);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
detectedFormat ??= rowFormat;
|
|
1130
|
+
if (requestedFormat && rowFormat !== requestedFormat) {
|
|
1131
|
+
errors.push(`Row ${rowNumber}: expected ${requestedFormat} row, found ${rowFormat}`);
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (detectedFormat !== rowFormat) {
|
|
1135
|
+
errors.push(`Row ${rowNumber}: cannot mix ${detectedFormat} and ${rowFormat} rows in one dataset`);
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
if (rowFormat === "document_ocr_jsonl") {
|
|
1139
|
+
validateDocumentOcrRow(record, rowNumber, errors);
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1029
1142
|
if (typeof record.input !== "string") {
|
|
1030
1143
|
errors.push(`Row ${rowNumber}: missing string "input" field`);
|
|
1031
1144
|
}
|
|
@@ -1051,10 +1164,11 @@ function validateDatasetFile(file) {
|
|
|
1051
1164
|
const suffix = errors.length > 5 ? `
|
|
1052
1165
|
...and ${errors.length - 5} more error(s)` : "";
|
|
1053
1166
|
throw new Error(
|
|
1054
|
-
`Invalid dataset format. Each JSONL row must be
|
|
1167
|
+
`Invalid dataset format. Each JSONL row must be a valid text or document OCR example.
|
|
1055
1168
|
${preview}${suffix}`
|
|
1056
1169
|
);
|
|
1057
1170
|
}
|
|
1171
|
+
return detectedFormat ?? requestedFormat ?? "jsonl";
|
|
1058
1172
|
}
|
|
1059
1173
|
function registerDatasetsCommands(parent) {
|
|
1060
1174
|
const datasets = parent.command("datasets").description("Manage datasets");
|
|
@@ -1102,13 +1216,13 @@ function registerDatasetsCommands(parent) {
|
|
|
1102
1216
|
}
|
|
1103
1217
|
}
|
|
1104
1218
|
});
|
|
1105
|
-
datasets.command("upload").description("Upload a JSONL dataset file").argument("<file>", "Path to JSONL file").option("-n, --name <name>", "Dataset name (defaults to filename)").option("-d, --description <desc>", "Dataset description").action(async (file, cmdOpts) => {
|
|
1219
|
+
datasets.command("upload").description("Upload a JSONL dataset file").argument("<file>", "Path to JSONL file").option("-n, --name <name>", "Dataset name (defaults to filename)").option("-d, --description <desc>", "Dataset description").option("--format <format>", "Dataset format: jsonl or document_ocr_jsonl").action(async (file, cmdOpts) => {
|
|
1106
1220
|
const opts = parent.opts();
|
|
1107
1221
|
if (!existsSync2(file)) {
|
|
1108
1222
|
printError(`File not found: ${file}`);
|
|
1109
1223
|
process.exit(1);
|
|
1110
1224
|
}
|
|
1111
|
-
validateDatasetFile(file);
|
|
1225
|
+
const format = validateDatasetFile(file, parseDatasetFormat(cmdOpts.format));
|
|
1112
1226
|
const name = cmdOpts.name || file.split("/").pop().replace(/\.jsonl?$/, "");
|
|
1113
1227
|
const fileBytes = readFileSync4(file);
|
|
1114
1228
|
const uploadUrl = await post(
|
|
@@ -1118,7 +1232,8 @@ function registerDatasetsCommands(parent) {
|
|
|
1118
1232
|
description: cmdOpts.description ?? null,
|
|
1119
1233
|
filename: file.split("/").pop(),
|
|
1120
1234
|
size: statSync2(file).size,
|
|
1121
|
-
contentType: "application/jsonl"
|
|
1235
|
+
contentType: "application/jsonl",
|
|
1236
|
+
format
|
|
1122
1237
|
},
|
|
1123
1238
|
opts
|
|
1124
1239
|
);
|
|
@@ -1153,27 +1268,425 @@ function registerDatasetsCommands(parent) {
|
|
|
1153
1268
|
});
|
|
1154
1269
|
}
|
|
1155
1270
|
|
|
1271
|
+
// src/commands/label.ts
|
|
1272
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
1273
|
+
function formatCents2(cents) {
|
|
1274
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
1275
|
+
}
|
|
1276
|
+
function sleep(ms) {
|
|
1277
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1278
|
+
}
|
|
1279
|
+
function parseCsvHeader(line) {
|
|
1280
|
+
const fields = [];
|
|
1281
|
+
let current = "";
|
|
1282
|
+
let inQuotes = false;
|
|
1283
|
+
for (let i = 0; i < line.length; i++) {
|
|
1284
|
+
const ch = line[i];
|
|
1285
|
+
if (inQuotes) {
|
|
1286
|
+
if (ch === '"') {
|
|
1287
|
+
if (line[i + 1] === '"') {
|
|
1288
|
+
current += '"';
|
|
1289
|
+
i++;
|
|
1290
|
+
} else {
|
|
1291
|
+
inQuotes = false;
|
|
1292
|
+
}
|
|
1293
|
+
} else {
|
|
1294
|
+
current += ch;
|
|
1295
|
+
}
|
|
1296
|
+
} else if (ch === '"') {
|
|
1297
|
+
inQuotes = true;
|
|
1298
|
+
} else if (ch === ",") {
|
|
1299
|
+
fields.push(current);
|
|
1300
|
+
current = "";
|
|
1301
|
+
} else {
|
|
1302
|
+
current += ch;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
fields.push(current);
|
|
1306
|
+
return fields.map((f) => f.trim());
|
|
1307
|
+
}
|
|
1308
|
+
function validateUnlabeledFile(file, inputColumn) {
|
|
1309
|
+
const lower = file.toLowerCase();
|
|
1310
|
+
if (lower.endsWith(".csv")) {
|
|
1311
|
+
if (!inputColumn) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
"CSV uploads need --input-column <name> to say which column holds the input text."
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
const firstLine = readFileSync5(file, "utf8").split(/\r?\n/, 1)[0] ?? "";
|
|
1317
|
+
const columns = parseCsvHeader(firstLine);
|
|
1318
|
+
if (!columns.includes(inputColumn)) {
|
|
1319
|
+
throw new Error(
|
|
1320
|
+
`Column "${inputColumn}" not found in CSV header. Available columns: ${columns.join(", ")}`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
return { format: "csv" };
|
|
1324
|
+
}
|
|
1325
|
+
if (!lower.endsWith(".jsonl") && !lower.endsWith(".json")) {
|
|
1326
|
+
throw new Error("Labeling source file must be .jsonl or .csv");
|
|
1327
|
+
}
|
|
1328
|
+
const lines = readFileSync5(file, "utf8").split(/\r?\n/);
|
|
1329
|
+
const errors = [];
|
|
1330
|
+
let rowCount = 0;
|
|
1331
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
1332
|
+
const line = rawLine.trim();
|
|
1333
|
+
if (!line) continue;
|
|
1334
|
+
rowCount += 1;
|
|
1335
|
+
let row;
|
|
1336
|
+
try {
|
|
1337
|
+
row = JSON.parse(line);
|
|
1338
|
+
} catch {
|
|
1339
|
+
errors.push(`Row ${index + 1}: invalid JSON`);
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.input !== "string") {
|
|
1343
|
+
errors.push(`Row ${index + 1}: expected an object with a string "input" field`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (rowCount === 0) {
|
|
1347
|
+
errors.push("File contains no JSONL rows");
|
|
1348
|
+
}
|
|
1349
|
+
if (errors.length > 0) {
|
|
1350
|
+
const preview = errors.slice(0, 5).join("\n");
|
|
1351
|
+
const suffix = errors.length > 5 ? `
|
|
1352
|
+
...and ${errors.length - 5} more error(s)` : "";
|
|
1353
|
+
throw new Error(
|
|
1354
|
+
`Invalid labeling source. Each JSONL row must be {"input": "..."}.
|
|
1355
|
+
${preview}${suffix}`
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
return { format: "jsonl" };
|
|
1359
|
+
}
|
|
1360
|
+
function printJobDetail(job, counts) {
|
|
1361
|
+
printDetail([
|
|
1362
|
+
["ID", job.id],
|
|
1363
|
+
["Name", job.name],
|
|
1364
|
+
["Status", formatStatus(job.status)],
|
|
1365
|
+
["Spec", shortId(job.behavior_spec_id)],
|
|
1366
|
+
["Teacher", job.teacher_model],
|
|
1367
|
+
["Rows", String(job.row_count)],
|
|
1368
|
+
["Labeled", String(job.labeled_count)],
|
|
1369
|
+
["Failed", String(job.failed_count)],
|
|
1370
|
+
[
|
|
1371
|
+
"Cost",
|
|
1372
|
+
job.actual_cost_cents !== null ? formatCents2(job.actual_cost_cents) : `~${formatCents2(job.est_cost_cents)} (estimated)`
|
|
1373
|
+
],
|
|
1374
|
+
["Dataset", job.promoted_dataset_id ? shortId(job.promoted_dataset_id) : void 0],
|
|
1375
|
+
["Error", job.error ?? void 0],
|
|
1376
|
+
["Created", formatDate(job.created_at)]
|
|
1377
|
+
]);
|
|
1378
|
+
if (counts) {
|
|
1379
|
+
console.log(
|
|
1380
|
+
`
|
|
1381
|
+
Review: ${counts.labeled} awaiting \xB7 ${counts.accepted} accepted \xB7 ${counts.edited} edited \xB7 ${counts.rejected} rejected \xB7 ${counts.failed} failed`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async function watchJob(jobId, opts) {
|
|
1386
|
+
for (; ; ) {
|
|
1387
|
+
const { data } = await get(
|
|
1388
|
+
`/labeling-jobs/${jobId}`,
|
|
1389
|
+
void 0,
|
|
1390
|
+
opts
|
|
1391
|
+
);
|
|
1392
|
+
if (!isJsonMode()) {
|
|
1393
|
+
if (data.status === "preparing") {
|
|
1394
|
+
process.stdout.write("\rPreparing - parsing the source file... ");
|
|
1395
|
+
} else {
|
|
1396
|
+
const done = data.counts.total - data.counts.pending;
|
|
1397
|
+
process.stdout.write(`\rLabeling ${done}/${data.counts.total} rows `);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (data.status !== "preparing" && data.status !== "labeling") {
|
|
1401
|
+
if (!isJsonMode()) process.stdout.write("\n");
|
|
1402
|
+
return data;
|
|
1403
|
+
}
|
|
1404
|
+
await sleep(5e3);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
async function resolveRowIds(jobId, indexes, opts) {
|
|
1408
|
+
const PER_PAGE = 100;
|
|
1409
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
1410
|
+
for (const index of indexes) {
|
|
1411
|
+
const page = Math.floor(index / PER_PAGE) + 1;
|
|
1412
|
+
byPage.set(page, [...byPage.get(page) ?? [], index]);
|
|
1413
|
+
}
|
|
1414
|
+
const found = /* @__PURE__ */ new Map();
|
|
1415
|
+
for (const [page, wanted] of byPage) {
|
|
1416
|
+
const { data } = await get(
|
|
1417
|
+
`/labeling-jobs/${jobId}/rows`,
|
|
1418
|
+
{ page, per_page: PER_PAGE },
|
|
1419
|
+
opts
|
|
1420
|
+
);
|
|
1421
|
+
for (const row of data) {
|
|
1422
|
+
if (wanted.includes(row.row_index)) {
|
|
1423
|
+
found.set(row.row_index, row.id);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
const missing = indexes.filter((i) => !found.has(i));
|
|
1428
|
+
if (missing.length > 0) {
|
|
1429
|
+
throw new Error(`Row index(es) not found: ${missing.join(", ")}`);
|
|
1430
|
+
}
|
|
1431
|
+
return found;
|
|
1432
|
+
}
|
|
1433
|
+
function parseIndexList(value) {
|
|
1434
|
+
const indexes = value.split(",").map((part) => Number.parseInt(part.trim(), 10));
|
|
1435
|
+
if (indexes.some((n) => Number.isNaN(n) || n < 0)) {
|
|
1436
|
+
throw new Error(`Invalid row index list: "${value}" (expected e.g. "0,3,12")`);
|
|
1437
|
+
}
|
|
1438
|
+
return indexes;
|
|
1439
|
+
}
|
|
1440
|
+
function registerLabelCommands(parent) {
|
|
1441
|
+
const label = parent.command("label").description("Teacher-label real data into training datasets");
|
|
1442
|
+
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) => {
|
|
1443
|
+
const opts = parent.opts();
|
|
1444
|
+
if (!existsSync3(file)) {
|
|
1445
|
+
printError(`File not found: ${file}`);
|
|
1446
|
+
process.exit(1);
|
|
1447
|
+
}
|
|
1448
|
+
const { format } = validateUnlabeledFile(file, cmdOpts.inputColumn);
|
|
1449
|
+
const specId = await resolveSpecId(cmdOpts.spec, opts);
|
|
1450
|
+
const name = cmdOpts.name || file.split("/").pop().replace(/\.(jsonl|json|csv)$/, "");
|
|
1451
|
+
const fileBytes = readFileSync5(file);
|
|
1452
|
+
const contentType = format === "csv" ? "text/csv" : "application/jsonl";
|
|
1453
|
+
const { data: uploadUrl } = await post(
|
|
1454
|
+
"/labeling-jobs/upload-url",
|
|
1455
|
+
{
|
|
1456
|
+
filename: file.split("/").pop(),
|
|
1457
|
+
size: statSync3(file).size,
|
|
1458
|
+
contentType
|
|
1459
|
+
},
|
|
1460
|
+
opts
|
|
1461
|
+
);
|
|
1462
|
+
const uploadRes = await fetch(uploadUrl.upload_url, {
|
|
1463
|
+
method: uploadUrl.method,
|
|
1464
|
+
headers: uploadUrl.headers ?? { "Content-Type": contentType },
|
|
1465
|
+
body: new Blob([fileBytes], { type: contentType })
|
|
1466
|
+
});
|
|
1467
|
+
if (!uploadRes.ok) {
|
|
1468
|
+
throw new Error(`Upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
|
|
1469
|
+
}
|
|
1470
|
+
const { data: job } = await post(
|
|
1471
|
+
"/labeling-jobs",
|
|
1472
|
+
{
|
|
1473
|
+
path: uploadUrl.path,
|
|
1474
|
+
name,
|
|
1475
|
+
behavior_spec_id: specId,
|
|
1476
|
+
source_format: format,
|
|
1477
|
+
input_column: cmdOpts.inputColumn ?? null
|
|
1478
|
+
},
|
|
1479
|
+
opts
|
|
1480
|
+
);
|
|
1481
|
+
if (!cmdOpts.watch) {
|
|
1482
|
+
if (isJsonMode()) return printJson(job);
|
|
1483
|
+
printSuccess(
|
|
1484
|
+
`Labeling started: ${job.name} (${shortId(job.id)}) \u2014 est. ${formatCents2(job.est_cost_cents)}. Runs in the cloud; no need to stay connected.`
|
|
1485
|
+
);
|
|
1486
|
+
console.log(`Follow progress with \`tt label watch ${shortId(job.id)}\`.`);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const finished = await watchJob(job.id, opts);
|
|
1490
|
+
if (isJsonMode()) return printJson(finished);
|
|
1491
|
+
printSuccess(
|
|
1492
|
+
`Labeling ${finished.status === "awaiting_review" ? "complete" : finished.status}: ${finished.labeled_count}/${finished.row_count} rows labeled.`
|
|
1493
|
+
);
|
|
1494
|
+
console.log(
|
|
1495
|
+
`Review with \`tt label rows ${shortId(job.id)} --status labeled\`, then \`tt label promote ${shortId(job.id)} --name <dataset-name>\`.`
|
|
1496
|
+
);
|
|
1497
|
+
});
|
|
1498
|
+
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) => {
|
|
1499
|
+
const opts = parent.opts();
|
|
1500
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1501
|
+
const job = await watchJob(jobId, opts);
|
|
1502
|
+
if (isJsonMode()) return printJson(job);
|
|
1503
|
+
if (job.status === "awaiting_review") {
|
|
1504
|
+
printSuccess(
|
|
1505
|
+
`Labeling complete: ${job.labeled_count}/${job.row_count} rows labeled, cost ${job.actual_cost_cents !== null ? formatCents2(job.actual_cost_cents) : "n/a"}.`
|
|
1506
|
+
);
|
|
1507
|
+
} else {
|
|
1508
|
+
printError(`Labeling ended with status: ${job.status}${job.error ? ` \u2014 ${job.error}` : ""}`);
|
|
1509
|
+
process.exitCode = 1;
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
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) => {
|
|
1513
|
+
const opts = parent.opts();
|
|
1514
|
+
const { data, meta } = await get(
|
|
1515
|
+
"/labeling-jobs",
|
|
1516
|
+
{ page: cmdOpts.page, per_page: cmdOpts.perPage },
|
|
1517
|
+
opts
|
|
1518
|
+
);
|
|
1519
|
+
if (isJsonMode()) return printJson({ data, meta });
|
|
1520
|
+
printTable(
|
|
1521
|
+
["ID", "Name", "Status", "Progress", "Cost", "Created"],
|
|
1522
|
+
data.map((j) => [
|
|
1523
|
+
shortId(j.id),
|
|
1524
|
+
truncate(j.name, 30),
|
|
1525
|
+
formatStatus(j.status),
|
|
1526
|
+
`${j.labeled_count}/${j.row_count}`,
|
|
1527
|
+
j.actual_cost_cents !== null ? formatCents2(j.actual_cost_cents) : `~${formatCents2(j.est_cost_cents)}`,
|
|
1528
|
+
formatDate(j.created_at)
|
|
1529
|
+
]),
|
|
1530
|
+
meta
|
|
1531
|
+
);
|
|
1532
|
+
});
|
|
1533
|
+
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) => {
|
|
1534
|
+
const opts = parent.opts();
|
|
1535
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1536
|
+
const { data } = await get(
|
|
1537
|
+
`/labeling-jobs/${jobId}`,
|
|
1538
|
+
void 0,
|
|
1539
|
+
opts
|
|
1540
|
+
);
|
|
1541
|
+
if (isJsonMode()) return printJson(data);
|
|
1542
|
+
printJobDetail(data, data.counts);
|
|
1543
|
+
});
|
|
1544
|
+
label.command("rows").description("List rows in a labeling job").argument("<id>", "Labeling job ID (full UUID or 4+ char prefix)").option(
|
|
1545
|
+
"--status <status>",
|
|
1546
|
+
"Filter by row status (pending|labeled|accepted|edited|rejected|failed)"
|
|
1547
|
+
).option("-p, --page <n>", "Page number", "1").option("--per-page <n>", "Results per page", "50").action(async (id, cmdOpts) => {
|
|
1548
|
+
const opts = parent.opts();
|
|
1549
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1550
|
+
const query = {
|
|
1551
|
+
page: cmdOpts.page,
|
|
1552
|
+
per_page: cmdOpts.perPage
|
|
1553
|
+
};
|
|
1554
|
+
if (cmdOpts.status) query.status = cmdOpts.status;
|
|
1555
|
+
const { data, meta } = await get(
|
|
1556
|
+
`/labeling-jobs/${jobId}/rows`,
|
|
1557
|
+
query,
|
|
1558
|
+
opts
|
|
1559
|
+
);
|
|
1560
|
+
if (isJsonMode()) return printJson({ data, meta });
|
|
1561
|
+
printTable(
|
|
1562
|
+
["Row", "Status", "Input", "Output"],
|
|
1563
|
+
data.map((row) => [
|
|
1564
|
+
String(row.row_index),
|
|
1565
|
+
formatStatus(row.status),
|
|
1566
|
+
truncate(row.input.replace(/\s+/g, " "), 40),
|
|
1567
|
+
truncate(
|
|
1568
|
+
(row.final_output ?? row.teacher_output ?? "\u2014").replace(/\s+/g, " "),
|
|
1569
|
+
40
|
|
1570
|
+
)
|
|
1571
|
+
]),
|
|
1572
|
+
meta
|
|
1573
|
+
);
|
|
1574
|
+
});
|
|
1575
|
+
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) => {
|
|
1576
|
+
const opts = parent.opts();
|
|
1577
|
+
if (!cmdOpts.all && !cmdOpts.rows) {
|
|
1578
|
+
printError("Provide --all or --rows <indexes>.");
|
|
1579
|
+
process.exit(1);
|
|
1580
|
+
}
|
|
1581
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1582
|
+
if (cmdOpts.all) {
|
|
1583
|
+
const { data } = await post(
|
|
1584
|
+
`/labeling-jobs/${jobId}/rows/bulk`,
|
|
1585
|
+
{ action: "accept", all_labeled: true },
|
|
1586
|
+
opts
|
|
1587
|
+
);
|
|
1588
|
+
if (isJsonMode()) return printJson(data);
|
|
1589
|
+
return printSuccess(`Accepted ${data.accepted} rows.`);
|
|
1590
|
+
}
|
|
1591
|
+
const indexes = parseIndexList(cmdOpts.rows);
|
|
1592
|
+
const rowIds = await resolveRowIds(jobId, indexes, opts);
|
|
1593
|
+
for (const index of indexes) {
|
|
1594
|
+
await patch(
|
|
1595
|
+
`/labeling-jobs/${jobId}/rows/${rowIds.get(index)}`,
|
|
1596
|
+
{ action: "accept" },
|
|
1597
|
+
opts
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
if (isJsonMode()) return printJson({ accepted: indexes.length });
|
|
1601
|
+
printSuccess(`Accepted ${indexes.length} row(s).`);
|
|
1602
|
+
});
|
|
1603
|
+
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) => {
|
|
1604
|
+
const opts = parent.opts();
|
|
1605
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1606
|
+
const indexes = parseIndexList(cmdOpts.rows);
|
|
1607
|
+
const rowIds = await resolveRowIds(jobId, indexes, opts);
|
|
1608
|
+
for (const index of indexes) {
|
|
1609
|
+
await patch(
|
|
1610
|
+
`/labeling-jobs/${jobId}/rows/${rowIds.get(index)}`,
|
|
1611
|
+
{ action: "reject" },
|
|
1612
|
+
opts
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
if (isJsonMode()) return printJson({ rejected: indexes.length });
|
|
1616
|
+
printSuccess(`Rejected ${indexes.length} row(s).`);
|
|
1617
|
+
});
|
|
1618
|
+
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) => {
|
|
1619
|
+
const opts = parent.opts();
|
|
1620
|
+
if (!cmdOpts.output && !cmdOpts.file) {
|
|
1621
|
+
printError("Provide --output <text> or --file <path>.");
|
|
1622
|
+
process.exit(1);
|
|
1623
|
+
}
|
|
1624
|
+
const output = cmdOpts.output ?? readFileSync5(cmdOpts.file, "utf8");
|
|
1625
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1626
|
+
const indexes = parseIndexList(cmdOpts.row);
|
|
1627
|
+
const rowIds = await resolveRowIds(jobId, indexes, opts);
|
|
1628
|
+
await patch(
|
|
1629
|
+
`/labeling-jobs/${jobId}/rows/${rowIds.get(indexes[0])}`,
|
|
1630
|
+
{ action: "edit", output },
|
|
1631
|
+
opts
|
|
1632
|
+
);
|
|
1633
|
+
if (isJsonMode()) return printJson({ edited: indexes[0] });
|
|
1634
|
+
printSuccess(`Row ${indexes[0]} edited.`);
|
|
1635
|
+
});
|
|
1636
|
+
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) => {
|
|
1637
|
+
const opts = parent.opts();
|
|
1638
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1639
|
+
const { data } = await post(
|
|
1640
|
+
`/labeling-jobs/${jobId}/promote`,
|
|
1641
|
+
{
|
|
1642
|
+
name: cmdOpts.name,
|
|
1643
|
+
description: cmdOpts.description ?? null,
|
|
1644
|
+
include_unreviewed_labeled: Boolean(cmdOpts.includeUnreviewed)
|
|
1645
|
+
},
|
|
1646
|
+
opts
|
|
1647
|
+
);
|
|
1648
|
+
if (isJsonMode()) return printJson(data);
|
|
1649
|
+
printSuccess(
|
|
1650
|
+
`Dataset created: ${data.name} (${shortId(data.id)}) \u2014 ${data.row_count} rows.`
|
|
1651
|
+
);
|
|
1652
|
+
console.log(
|
|
1653
|
+
`Start a run with \`tt runs start <spec-id> --dataset ${shortId(data.id)}\`.`
|
|
1654
|
+
);
|
|
1655
|
+
});
|
|
1656
|
+
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) => {
|
|
1657
|
+
const opts = parent.opts();
|
|
1658
|
+
const jobId = await resolveLabelingJobId(id, opts);
|
|
1659
|
+
const { data } = await post(
|
|
1660
|
+
`/labeling-jobs/${jobId}/cancel`,
|
|
1661
|
+
{},
|
|
1662
|
+
opts
|
|
1663
|
+
);
|
|
1664
|
+
if (isJsonMode()) return printJson(data);
|
|
1665
|
+
printSuccess(`Labeling job cancelled: ${shortId(jobId)}`);
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1156
1669
|
// src/commands/models.ts
|
|
1157
1670
|
import { spawn as spawn2, execFileSync } from "child_process";
|
|
1158
1671
|
import { createHash } from "crypto";
|
|
1159
1672
|
import {
|
|
1160
|
-
existsSync as
|
|
1673
|
+
existsSync as existsSync5,
|
|
1161
1674
|
mkdirSync as mkdirSync3,
|
|
1162
|
-
statSync as
|
|
1675
|
+
statSync as statSync4,
|
|
1163
1676
|
createWriteStream,
|
|
1164
1677
|
unlinkSync,
|
|
1165
|
-
readFileSync as
|
|
1678
|
+
readFileSync as readFileSync7,
|
|
1166
1679
|
writeFileSync as writeFileSync3,
|
|
1167
1680
|
readdirSync,
|
|
1168
1681
|
rmSync
|
|
1169
1682
|
} from "fs";
|
|
1170
1683
|
import { homedir as homedir2 } from "os";
|
|
1171
|
-
import { dirname as dirname2, join as join2, resolve as resolve2, basename as basename2 } from "path";
|
|
1684
|
+
import { dirname as dirname2, join as join2, resolve as resolve2, basename as basename2, normalize, isAbsolute } from "path";
|
|
1172
1685
|
import { Readable, Transform } from "stream";
|
|
1173
1686
|
import { pipeline } from "stream/promises";
|
|
1174
1687
|
|
|
1175
1688
|
// src/commands/init.ts
|
|
1176
|
-
import { existsSync as
|
|
1689
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
|
|
1177
1690
|
import { resolve } from "path";
|
|
1178
1691
|
var DEFAULT_SPEC_FILE = "tunedtensor.json";
|
|
1179
1692
|
var SCAFFOLD = {
|
|
@@ -1191,7 +1704,7 @@ var SCAFFOLD = {
|
|
|
1191
1704
|
function registerInitCommand(parent) {
|
|
1192
1705
|
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
1706
|
const filePath = resolve(cmdOpts.file);
|
|
1194
|
-
if (
|
|
1707
|
+
if (existsSync4(filePath)) {
|
|
1195
1708
|
printWarning(`${cmdOpts.file} already exists. Use tt eval to validate it or edit it directly.`);
|
|
1196
1709
|
return;
|
|
1197
1710
|
}
|
|
@@ -1208,14 +1721,14 @@ function registerInitCommand(parent) {
|
|
|
1208
1721
|
}
|
|
1209
1722
|
function loadSpec(filePath) {
|
|
1210
1723
|
const resolved = resolve(filePath || DEFAULT_SPEC_FILE);
|
|
1211
|
-
if (!
|
|
1724
|
+
if (!existsSync4(resolved)) {
|
|
1212
1725
|
printError(
|
|
1213
1726
|
`Spec file not found: ${filePath || DEFAULT_SPEC_FILE}
|
|
1214
1727
|
Run \`tt init\` to create one.`
|
|
1215
1728
|
);
|
|
1216
1729
|
process.exit(1);
|
|
1217
1730
|
}
|
|
1218
|
-
const raw = JSON.parse(
|
|
1731
|
+
const raw = JSON.parse(readFileSync6(resolved, "utf-8"));
|
|
1219
1732
|
return raw;
|
|
1220
1733
|
}
|
|
1221
1734
|
|
|
@@ -1230,7 +1743,7 @@ var COMPLETION_ROUTES = /* @__PURE__ */ new Set(["/v1/chat/completions", "/chat/
|
|
|
1230
1743
|
function parsePositiveInteger(value, fallback) {
|
|
1231
1744
|
return Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
1232
1745
|
}
|
|
1233
|
-
function
|
|
1746
|
+
function sleep2(ms) {
|
|
1234
1747
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1235
1748
|
}
|
|
1236
1749
|
function readRequestBody(req) {
|
|
@@ -1552,7 +2065,7 @@ var ManagedModelServer = class {
|
|
|
1552
2065
|
child.kill("SIGTERM");
|
|
1553
2066
|
await Promise.race([
|
|
1554
2067
|
new Promise((resolve4) => child.once("exit", () => resolve4())),
|
|
1555
|
-
|
|
2068
|
+
sleep2(5e3).then(() => {
|
|
1556
2069
|
if (child.exitCode == null && child.signalCode == null) child.kill("SIGKILL");
|
|
1557
2070
|
})
|
|
1558
2071
|
]);
|
|
@@ -1582,7 +2095,7 @@ var ManagedModelServer = class {
|
|
|
1582
2095
|
const health = await this.checkHealth();
|
|
1583
2096
|
if (health.ok) return;
|
|
1584
2097
|
if (!this.child) break;
|
|
1585
|
-
await
|
|
2098
|
+
await sleep2(50);
|
|
1586
2099
|
}
|
|
1587
2100
|
throw new Error("Model server did not become healthy within 120 seconds");
|
|
1588
2101
|
}
|
|
@@ -1663,7 +2176,10 @@ import json
|
|
|
1663
2176
|
import os
|
|
1664
2177
|
import time
|
|
1665
2178
|
import uuid
|
|
2179
|
+
import base64
|
|
2180
|
+
from io import BytesIO
|
|
1666
2181
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
2182
|
+
from urllib.request import urlopen
|
|
1667
2183
|
|
|
1668
2184
|
MODEL_PATH = os.environ["TT_MODEL_PATH"]
|
|
1669
2185
|
MODEL_NAME = os.environ.get("TT_MODEL_NAME", os.path.basename(MODEL_PATH.rstrip("/")) or "tuned-tensor-model")
|
|
@@ -1714,9 +2230,9 @@ def load_model():
|
|
|
1714
2230
|
) from exc
|
|
1715
2231
|
|
|
1716
2232
|
try:
|
|
1717
|
-
processor = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=TRUST_REMOTE_CODE)
|
|
1718
|
-
except Exception:
|
|
1719
2233
|
processor = AutoProcessor.from_pretrained(MODEL_PATH, trust_remote_code=TRUST_REMOTE_CODE)
|
|
2234
|
+
except Exception:
|
|
2235
|
+
processor = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=TRUST_REMOTE_CODE)
|
|
1720
2236
|
tokenizer = getattr(processor, "tokenizer", processor)
|
|
1721
2237
|
if getattr(tokenizer, "pad_token", None) is None:
|
|
1722
2238
|
tokenizer.pad_token = tokenizer.eos_token
|
|
@@ -1733,22 +2249,43 @@ def load_model():
|
|
|
1733
2249
|
kwargs["torch_dtype"] = torch.float16
|
|
1734
2250
|
|
|
1735
2251
|
try:
|
|
1736
|
-
model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, **kwargs)
|
|
1737
|
-
except Exception:
|
|
1738
2252
|
model = AutoModelForImageTextToText.from_pretrained(MODEL_PATH, **kwargs)
|
|
2253
|
+
except Exception:
|
|
2254
|
+
model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, **kwargs)
|
|
1739
2255
|
if device != "cuda":
|
|
1740
2256
|
model.to(device)
|
|
1741
2257
|
model.eval()
|
|
1742
|
-
return torch, tokenizer, model, device
|
|
2258
|
+
return torch, processor, tokenizer, model, device
|
|
2259
|
+
|
|
2260
|
+
|
|
2261
|
+
torch, processor, tokenizer, model, DEVICE = load_model()
|
|
1743
2262
|
|
|
1744
2263
|
|
|
1745
|
-
|
|
2264
|
+
def normalize_content_part(part):
|
|
2265
|
+
if not isinstance(part, dict):
|
|
2266
|
+
return {"type": "text", "text": str(part)}
|
|
2267
|
+
part_type = part.get("type")
|
|
2268
|
+
if part_type == "text":
|
|
2269
|
+
return {"type": "text", "text": str(part.get("text", ""))}
|
|
2270
|
+
if part_type in {"image", "image_url"}:
|
|
2271
|
+
image_url = part.get("image_url")
|
|
2272
|
+
url = None
|
|
2273
|
+
if isinstance(image_url, dict):
|
|
2274
|
+
url = image_url.get("url")
|
|
2275
|
+
elif isinstance(image_url, str):
|
|
2276
|
+
url = image_url
|
|
2277
|
+
url = url or part.get("image") or part.get("data_uri") or part.get("uri") or part.get("path")
|
|
2278
|
+
if isinstance(url, str) and url.strip():
|
|
2279
|
+
return {"type": "image", "image": url.strip()}
|
|
2280
|
+
return {"type": "text", "text": json.dumps(part, ensure_ascii=False)}
|
|
1746
2281
|
|
|
1747
2282
|
|
|
1748
|
-
def
|
|
2283
|
+
def normalize_content(content):
|
|
1749
2284
|
if isinstance(content, str):
|
|
1750
2285
|
return content
|
|
1751
|
-
|
|
2286
|
+
if isinstance(content, list):
|
|
2287
|
+
return [normalize_content_part(part) for part in content]
|
|
2288
|
+
return str(content) if content is not None else ""
|
|
1752
2289
|
|
|
1753
2290
|
|
|
1754
2291
|
def normalize_messages(raw_messages):
|
|
@@ -1759,7 +2296,7 @@ def normalize_messages(raw_messages):
|
|
|
1759
2296
|
role = item.get("role")
|
|
1760
2297
|
if role not in {"system", "user", "assistant", "tool"}:
|
|
1761
2298
|
continue
|
|
1762
|
-
messages.append({"role": role, "content":
|
|
2299
|
+
messages.append({"role": role, "content": normalize_content(item.get("content", ""))})
|
|
1763
2300
|
|
|
1764
2301
|
if SYSTEM_PROMPT and not any(message["role"] == "system" for message in messages):
|
|
1765
2302
|
messages.insert(0, {"role": "system", "content": SYSTEM_PROMPT})
|
|
@@ -1803,7 +2340,10 @@ def with_json_contract(messages, mode, schema):
|
|
|
1803
2340
|
contracted = [dict(message) for message in messages]
|
|
1804
2341
|
for message in contracted:
|
|
1805
2342
|
if message["role"] == "system":
|
|
1806
|
-
|
|
2343
|
+
content = message.get("content", "")
|
|
2344
|
+
if not isinstance(content, str):
|
|
2345
|
+
content = json.dumps(content, ensure_ascii=False)
|
|
2346
|
+
message["content"] = (content.rstrip() + "\n\n" + contract).strip()
|
|
1807
2347
|
return contracted
|
|
1808
2348
|
|
|
1809
2349
|
contracted.insert(0, {"role": "system", "content": contract})
|
|
@@ -1822,13 +2362,71 @@ def render_prompt(messages):
|
|
|
1822
2362
|
return "\n".join(rendered)
|
|
1823
2363
|
|
|
1824
2364
|
|
|
1825
|
-
def
|
|
1826
|
-
|
|
1827
|
-
|
|
2365
|
+
def load_image(value):
|
|
2366
|
+
try:
|
|
2367
|
+
from PIL import Image
|
|
2368
|
+
except Exception as exc:
|
|
2369
|
+
raise RuntimeError(
|
|
2370
|
+
"Multimodal requests require Pillow. Install the serving runtime with "
|
|
2371
|
+
"'tt models setup-runtime --force' or install pillow in the selected Python environment."
|
|
2372
|
+
) from exc
|
|
2373
|
+
|
|
2374
|
+
if value.startswith("data:image/"):
|
|
2375
|
+
_, payload = value.split(",", 1)
|
|
2376
|
+
return Image.open(BytesIO(base64.b64decode(payload))).convert("RGB")
|
|
2377
|
+
if value.startswith("http://") or value.startswith("https://"):
|
|
2378
|
+
with urlopen(value, timeout=30) as response:
|
|
2379
|
+
return Image.open(BytesIO(response.read())).convert("RGB")
|
|
2380
|
+
return Image.open(value).convert("RGB")
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
def resolve_message_images(messages):
|
|
2384
|
+
resolved = []
|
|
2385
|
+
images = []
|
|
2386
|
+
for message in messages:
|
|
2387
|
+
content = message.get("content")
|
|
2388
|
+
if not isinstance(content, list):
|
|
2389
|
+
resolved.append(message)
|
|
2390
|
+
continue
|
|
2391
|
+
|
|
2392
|
+
parts = []
|
|
2393
|
+
for part in content:
|
|
2394
|
+
if not isinstance(part, dict) or part.get("type") != "image":
|
|
2395
|
+
parts.append(part)
|
|
2396
|
+
continue
|
|
2397
|
+
image_ref = part.get("image") or part.get("data_uri") or part.get("uri") or part.get("path")
|
|
2398
|
+
if not isinstance(image_ref, str) or not image_ref.strip():
|
|
2399
|
+
raise RuntimeError("Image content part must include image, image_url.url, data_uri, uri, or path")
|
|
2400
|
+
images.append(load_image(image_ref.strip()))
|
|
2401
|
+
parts.append({"type": "image"})
|
|
2402
|
+
resolved.append({**message, "content": parts})
|
|
2403
|
+
return resolved, images
|
|
2404
|
+
|
|
2405
|
+
|
|
2406
|
+
def move_inputs_to_device(inputs, device):
|
|
2407
|
+
moved = {}
|
|
2408
|
+
for key, value in inputs.items():
|
|
2409
|
+
moved[key] = value.to(device) if hasattr(value, "to") else value
|
|
2410
|
+
return moved
|
|
2411
|
+
|
|
2412
|
+
|
|
2413
|
+
def prepare_generation_inputs(messages):
|
|
2414
|
+
resolved_messages, images = resolve_message_images(messages)
|
|
2415
|
+
prompt = render_prompt(resolved_messages)
|
|
1828
2416
|
input_device = getattr(model, "device", None)
|
|
1829
2417
|
if input_device is None:
|
|
1830
2418
|
input_device = next(model.parameters()).device
|
|
1831
|
-
|
|
2419
|
+
if images:
|
|
2420
|
+
if processor is tokenizer:
|
|
2421
|
+
raise RuntimeError("This model artifact did not load a multimodal processor, so image inputs are not supported.")
|
|
2422
|
+
inputs = processor(text=[prompt], images=images, return_tensors="pt")
|
|
2423
|
+
else:
|
|
2424
|
+
inputs = tokenizer(prompt, return_tensors="pt")
|
|
2425
|
+
return move_inputs_to_device(inputs, input_device)
|
|
2426
|
+
|
|
2427
|
+
|
|
2428
|
+
def generate_completion(messages, max_tokens, temperature):
|
|
2429
|
+
inputs = prepare_generation_inputs(messages)
|
|
1832
2430
|
|
|
1833
2431
|
generate_kwargs = {
|
|
1834
2432
|
**inputs,
|
|
@@ -2091,7 +2689,7 @@ if __name__ == "__main__":
|
|
|
2091
2689
|
`;
|
|
2092
2690
|
function resolveOutputPath(output, filename) {
|
|
2093
2691
|
if (!output) return filename;
|
|
2094
|
-
if (
|
|
2692
|
+
if (existsSync5(output) && statSync4(output).isDirectory()) {
|
|
2095
2693
|
return join2(output, filename);
|
|
2096
2694
|
}
|
|
2097
2695
|
return output;
|
|
@@ -2212,7 +2810,7 @@ function runtimePythonPath(cacheDir) {
|
|
|
2212
2810
|
function resolveServePython(cacheDir, explicitPython) {
|
|
2213
2811
|
if (explicitPython) return explicitPython;
|
|
2214
2812
|
const managedPython = runtimePythonPath(cacheDir);
|
|
2215
|
-
return
|
|
2813
|
+
return existsSync5(managedPython) ? managedPython : "python3";
|
|
2216
2814
|
}
|
|
2217
2815
|
function commandExists(command) {
|
|
2218
2816
|
try {
|
|
@@ -2256,7 +2854,7 @@ function ensureServingRuntime(python, cacheDir) {
|
|
|
2256
2854
|
const check = `
|
|
2257
2855
|
import importlib.util
|
|
2258
2856
|
import json
|
|
2259
|
-
required = ["torch", "transformers", "accelerate", "safetensors"]
|
|
2857
|
+
required = ["torch", "transformers", "accelerate", "safetensors", "PIL"]
|
|
2260
2858
|
missing = [name for name in required if importlib.util.find_spec(name) is None]
|
|
2261
2859
|
print(json.dumps({"missing": missing}))
|
|
2262
2860
|
`;
|
|
@@ -2291,11 +2889,28 @@ function safeSegment(value) {
|
|
|
2291
2889
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "model";
|
|
2292
2890
|
}
|
|
2293
2891
|
function directoryHasFiles(path) {
|
|
2294
|
-
return
|
|
2892
|
+
return existsSync5(path) && statSync4(path).isDirectory() && readdirSync(path).length > 0;
|
|
2893
|
+
}
|
|
2894
|
+
function validateArchiveEntryPath(entry) {
|
|
2895
|
+
const normalized = normalize(entry).replace(/^[\\/]+/, "");
|
|
2896
|
+
if (!entry.trim() || isAbsolute(entry) || normalized === ".." || normalized.startsWith(`..${"/"}`) || normalized.startsWith(`..${"\\"}`)) {
|
|
2897
|
+
throw new Error(`Unsafe archive entry path: ${entry}`);
|
|
2898
|
+
}
|
|
2899
|
+
return normalized;
|
|
2900
|
+
}
|
|
2901
|
+
function validateArchiveEntries(archivePath) {
|
|
2902
|
+
const listing = execFileSync("tar", ["-tzf", archivePath], {
|
|
2903
|
+
encoding: "utf8",
|
|
2904
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2905
|
+
});
|
|
2906
|
+
for (const entry of listing.split(/\r?\n/)) {
|
|
2907
|
+
if (entry.trim()) validateArchiveEntryPath(entry);
|
|
2908
|
+
}
|
|
2295
2909
|
}
|
|
2296
2910
|
function extractArchive(archivePath, targetDir, force) {
|
|
2297
2911
|
if (directoryHasFiles(targetDir) && !force) return targetDir;
|
|
2298
|
-
|
|
2912
|
+
validateArchiveEntries(archivePath);
|
|
2913
|
+
if (existsSync5(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
2299
2914
|
mkdirSync3(targetDir, { recursive: true });
|
|
2300
2915
|
execFileSync("tar", ["-xzf", archivePath, "-C", targetDir], { stdio: "inherit" });
|
|
2301
2916
|
return targetDir;
|
|
@@ -2322,21 +2937,21 @@ function findSpecPath(explicitPath, modelPath) {
|
|
|
2322
2937
|
const candidates = explicitPath ? [explicitPath] : [join2(process.cwd(), DEFAULT_SPEC_FILE), join2(modelPath, DEFAULT_SPEC_FILE)];
|
|
2323
2938
|
for (const candidate of candidates) {
|
|
2324
2939
|
const resolved = resolve2(candidate);
|
|
2325
|
-
if (
|
|
2940
|
+
if (existsSync5(resolved) && statSync4(resolved).isFile()) return resolved;
|
|
2326
2941
|
}
|
|
2327
2942
|
return null;
|
|
2328
2943
|
}
|
|
2329
2944
|
function loadSystemPromptFromSpec(specPath) {
|
|
2330
|
-
const spec = JSON.parse(
|
|
2945
|
+
const spec = JSON.parse(readFileSync7(specPath, "utf8"));
|
|
2331
2946
|
return buildSystemPrompt(spec);
|
|
2332
2947
|
}
|
|
2333
2948
|
function loadJsonSchemaForServe(schemaPath) {
|
|
2334
2949
|
const resolved = resolve2(schemaPath);
|
|
2335
|
-
if (!
|
|
2950
|
+
if (!existsSync5(resolved) || !statSync4(resolved).isFile()) {
|
|
2336
2951
|
throw new Error(`JSON schema file not found: ${schemaPath}`);
|
|
2337
2952
|
}
|
|
2338
2953
|
try {
|
|
2339
|
-
const schema = JSON.parse(
|
|
2954
|
+
const schema = JSON.parse(readFileSync7(resolved, "utf8"));
|
|
2340
2955
|
return JSON.stringify(schema);
|
|
2341
2956
|
} catch (err) {
|
|
2342
2957
|
throw new Error(`JSON schema file is not valid JSON: ${schemaPath}`);
|
|
@@ -2350,9 +2965,9 @@ function writeReferenceServerScript(cacheDir) {
|
|
|
2350
2965
|
return scriptPath;
|
|
2351
2966
|
}
|
|
2352
2967
|
async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
|
|
2353
|
-
if (
|
|
2968
|
+
if (existsSync5(target)) {
|
|
2354
2969
|
const resolved = resolve2(target);
|
|
2355
|
-
const stats =
|
|
2970
|
+
const stats = statSync4(resolved);
|
|
2356
2971
|
if (stats.isDirectory()) {
|
|
2357
2972
|
return {
|
|
2358
2973
|
modelPath: resolved,
|
|
@@ -2376,7 +2991,7 @@ async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
|
|
|
2376
2991
|
const modelCacheDir = join2(cacheDir, fullId);
|
|
2377
2992
|
const archivePath = join2(modelCacheDir, data.filename);
|
|
2378
2993
|
const extractDir = join2(modelCacheDir, "model");
|
|
2379
|
-
if (!
|
|
2994
|
+
if (!existsSync5(archivePath) || forceDownload) {
|
|
2380
2995
|
await downloadUrlToFile(data.url, archivePath);
|
|
2381
2996
|
}
|
|
2382
2997
|
extractArchive(archivePath, extractDir, forceDownload);
|
|
@@ -2480,7 +3095,7 @@ function ollamaSlug(name) {
|
|
|
2480
3095
|
}
|
|
2481
3096
|
function firstExisting(candidates) {
|
|
2482
3097
|
for (const candidate of candidates) {
|
|
2483
|
-
if (candidate &&
|
|
3098
|
+
if (candidate && existsSync5(candidate) && statSync4(candidate).isFile()) {
|
|
2484
3099
|
return resolve2(candidate);
|
|
2485
3100
|
}
|
|
2486
3101
|
}
|
|
@@ -2492,7 +3107,7 @@ function llamaCppDir(explicit) {
|
|
|
2492
3107
|
function resolveConvertScript(opts, required) {
|
|
2493
3108
|
if (opts.convertScript) {
|
|
2494
3109
|
const resolved = resolve2(opts.convertScript);
|
|
2495
|
-
if (required && !
|
|
3110
|
+
if (required && !existsSync5(resolved)) {
|
|
2496
3111
|
throw new Error(`Conversion script not found: ${opts.convertScript}`);
|
|
2497
3112
|
}
|
|
2498
3113
|
return resolved;
|
|
@@ -2511,7 +3126,7 @@ function resolveConvertScript(opts, required) {
|
|
|
2511
3126
|
function resolveQuantizeBin(opts, required) {
|
|
2512
3127
|
if (opts.quantizeBin) {
|
|
2513
3128
|
const resolved = resolve2(opts.quantizeBin);
|
|
2514
|
-
if (required && !
|
|
3129
|
+
if (required && !existsSync5(resolved)) {
|
|
2515
3130
|
throw new Error(`llama-quantize binary not found: ${opts.quantizeBin}`);
|
|
2516
3131
|
}
|
|
2517
3132
|
return resolved;
|
|
@@ -2646,7 +3261,7 @@ function registerModelsCommands(parent) {
|
|
|
2646
3261
|
opts
|
|
2647
3262
|
);
|
|
2648
3263
|
const outputPath = resolveOutputPath(cmdOpts.output, data.filename);
|
|
2649
|
-
if (
|
|
3264
|
+
if (existsSync5(outputPath) && !cmdOpts.force) {
|
|
2650
3265
|
throw new Error(`Output file already exists: ${outputPath}. Use --force to overwrite.`);
|
|
2651
3266
|
}
|
|
2652
3267
|
const bytes = await downloadUrlToFile(data.url, outputPath);
|
|
@@ -2755,7 +3370,7 @@ function registerModelsCommands(parent) {
|
|
|
2755
3370
|
ollama: ollamaInfo
|
|
2756
3371
|
};
|
|
2757
3372
|
if (printOnly) return printExportPlan(plan);
|
|
2758
|
-
if (
|
|
3373
|
+
if (existsSync5(finalPath) && !cmdOpts.force) {
|
|
2759
3374
|
throw new Error(`Output already exists: ${finalPath}. Use --force to overwrite.`);
|
|
2760
3375
|
}
|
|
2761
3376
|
mkdirSync3(outputDir, { recursive: true });
|
|
@@ -2776,7 +3391,7 @@ function registerModelsCommands(parent) {
|
|
|
2776
3391
|
finalPath,
|
|
2777
3392
|
quantPlan.quantizeType
|
|
2778
3393
|
]);
|
|
2779
|
-
if (!cmdOpts.keepIntermediate && intermediatePath &&
|
|
3394
|
+
if (!cmdOpts.keepIntermediate && intermediatePath && existsSync5(intermediatePath)) {
|
|
2780
3395
|
rmSync(intermediatePath, { force: true });
|
|
2781
3396
|
}
|
|
2782
3397
|
}
|
|
@@ -2810,7 +3425,7 @@ function registerModelsCommands(parent) {
|
|
|
2810
3425
|
const venvDir = runtimeDir(cacheDir);
|
|
2811
3426
|
const venvPython = runtimePythonPath(cacheDir);
|
|
2812
3427
|
const python = pickRuntimePython(cmdOpts.python);
|
|
2813
|
-
const deps = ["torch", "transformers", "accelerate", "safetensors"];
|
|
3428
|
+
const deps = ["torch", "transformers", "accelerate", "safetensors", "pillow"];
|
|
2814
3429
|
const commands = setupRuntimeCommands(python, venvDir, deps);
|
|
2815
3430
|
if (cmdOpts.printCommand) {
|
|
2816
3431
|
const commandLines = commands.map((cmd) => cmd.map(quoteCommandPart).join(" "));
|
|
@@ -2829,10 +3444,10 @@ function registerModelsCommands(parent) {
|
|
|
2829
3444
|
]);
|
|
2830
3445
|
return;
|
|
2831
3446
|
}
|
|
2832
|
-
if (
|
|
3447
|
+
if (existsSync5(venvDir) && cmdOpts.force) {
|
|
2833
3448
|
rmSync(venvDir, { recursive: true, force: true });
|
|
2834
3449
|
}
|
|
2835
|
-
if (!
|
|
3450
|
+
if (!existsSync5(venvPython)) {
|
|
2836
3451
|
mkdirSync3(cacheDir, { recursive: true });
|
|
2837
3452
|
execFileSync(commands[0][0], commands[0].slice(1), { stdio: "inherit" });
|
|
2838
3453
|
}
|
|
@@ -2973,15 +3588,15 @@ var KIND_LABELS = {
|
|
|
2973
3588
|
refund: "Refund",
|
|
2974
3589
|
adjustment: "Adjustment"
|
|
2975
3590
|
};
|
|
2976
|
-
function
|
|
3591
|
+
function formatCents3(cents) {
|
|
2977
3592
|
const sign = cents < 0 ? "-" : "";
|
|
2978
3593
|
const abs = Math.abs(cents);
|
|
2979
3594
|
return `${sign}$${(abs / 100).toFixed(2)}`;
|
|
2980
3595
|
}
|
|
2981
3596
|
function formatSigned(cents) {
|
|
2982
|
-
if (cents > 0) return chalk3.green(`+${
|
|
2983
|
-
if (cents < 0) return chalk3.red(
|
|
2984
|
-
return
|
|
3597
|
+
if (cents > 0) return chalk3.green(`+${formatCents3(cents)}`);
|
|
3598
|
+
if (cents < 0) return chalk3.red(formatCents3(cents));
|
|
3599
|
+
return formatCents3(cents);
|
|
2985
3600
|
}
|
|
2986
3601
|
function registerBalanceCommands(parent) {
|
|
2987
3602
|
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 +3610,18 @@ function registerBalanceCommands(parent) {
|
|
|
2995
3610
|
return printJson({ balance, transactions });
|
|
2996
3611
|
}
|
|
2997
3612
|
const lowBalance = balance.balance_cents < 100;
|
|
2998
|
-
const balanceLine = lowBalance ? chalk3.red(
|
|
3613
|
+
const balanceLine = lowBalance ? chalk3.red(formatCents3(balance.balance_cents)) + chalk3.dim(" (low)") : chalk3.bold.green(formatCents3(balance.balance_cents));
|
|
2999
3614
|
printDetail([
|
|
3000
3615
|
["Credits", balanceLine],
|
|
3001
|
-
[
|
|
3616
|
+
[
|
|
3617
|
+
"Reserved",
|
|
3618
|
+
balance.reserved_cents > 0 ? formatCents3(balance.reserved_cents) : void 0
|
|
3619
|
+
],
|
|
3620
|
+
[
|
|
3621
|
+
"Available",
|
|
3622
|
+
balance.reserved_cents > 0 ? formatCents3(balance.available_cents) : void 0
|
|
3623
|
+
],
|
|
3624
|
+
["Lifetime top-ups", formatCents3(balance.lifetime_topup_cents)]
|
|
3002
3625
|
]);
|
|
3003
3626
|
if (transactions && transactions.length > 0) {
|
|
3004
3627
|
console.log(`
|
|
@@ -3009,7 +3632,7 @@ ${chalk3.bold("Recent transactions")}`);
|
|
|
3009
3632
|
formatDate(row.created_at),
|
|
3010
3633
|
KIND_LABELS[row.kind] ?? row.kind,
|
|
3011
3634
|
formatSigned(row.amount_cents),
|
|
3012
|
-
|
|
3635
|
+
formatCents3(row.balance_after_cents),
|
|
3013
3636
|
row.reference_id ? shortId(row.reference_id) : chalk3.dim("\u2014")
|
|
3014
3637
|
])
|
|
3015
3638
|
);
|
|
@@ -3307,7 +3930,7 @@ function printValidation(checks) {
|
|
|
3307
3930
|
}
|
|
3308
3931
|
|
|
3309
3932
|
// src/commands/push.ts
|
|
3310
|
-
import { readFileSync as
|
|
3933
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
3311
3934
|
import { resolve as resolve3 } from "path";
|
|
3312
3935
|
function registerPushCommand(parent) {
|
|
3313
3936
|
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 +3950,7 @@ function registerPushCommand(parent) {
|
|
|
3327
3950
|
} else {
|
|
3328
3951
|
const res = await post("/behavior-specs", body, opts);
|
|
3329
3952
|
data = res.data;
|
|
3330
|
-
const raw = JSON.parse(
|
|
3953
|
+
const raw = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
3331
3954
|
raw.id = data.id;
|
|
3332
3955
|
writeFileSync4(filePath, JSON.stringify(raw, null, 2) + "\n");
|
|
3333
3956
|
}
|
|
@@ -3340,7 +3963,7 @@ function registerPushCommand(parent) {
|
|
|
3340
3963
|
|
|
3341
3964
|
// src/index.ts
|
|
3342
3965
|
var program = new Command();
|
|
3343
|
-
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.
|
|
3966
|
+
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.21").option("-k, --api-key <key>", "API key (overrides stored key)").option(
|
|
3344
3967
|
"-u, --base-url <url>",
|
|
3345
3968
|
"API base URL (default: https://tunedtensor.com)"
|
|
3346
3969
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|
|
@@ -3354,6 +3977,7 @@ registerAuthCommands(program);
|
|
|
3354
3977
|
registerSpecsCommands(program);
|
|
3355
3978
|
registerRunsCommands(program);
|
|
3356
3979
|
registerDatasetsCommands(program);
|
|
3980
|
+
registerLabelCommands(program);
|
|
3357
3981
|
registerModelsCommands(program);
|
|
3358
3982
|
registerBalanceCommands(program);
|
|
3359
3983
|
registerTopupCommands(program);
|