@tuned-tensor/cli 0.4.20 → 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/dist/index.js +217 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -387,6 +387,7 @@ var SUPPORTED_BASE_MODELS = [
|
|
|
387
387
|
"google/gemma-4-E2B-it",
|
|
388
388
|
"google/gemma-4-E4B-it",
|
|
389
389
|
"Qwen/Qwen3.5-2B",
|
|
390
|
+
"Qwen/Qwen3-VL-2B-Instruct",
|
|
390
391
|
"Qwen/Qwen3.5-4B",
|
|
391
392
|
"meta-llama/Llama-3.2-3B-Instruct",
|
|
392
393
|
"microsoft/Phi-4-mini-instruct",
|
|
@@ -407,6 +408,9 @@ for (const [alias, model] of [
|
|
|
407
408
|
["qwen/qwen3.5-2b", "Qwen/Qwen3.5-2B"],
|
|
408
409
|
["Qwen/Qwen3.5-2B-Base", "Qwen/Qwen3.5-2B"],
|
|
409
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"],
|
|
410
414
|
["qwen/qwen3.5-4b", "Qwen/Qwen3.5-4B"],
|
|
411
415
|
["Qwen/Qwen3.5-4B-Base", "Qwen/Qwen3.5-4B"],
|
|
412
416
|
["qwen/qwen3.5-4b-base", "Qwen/Qwen3.5-4B"],
|
|
@@ -959,6 +963,7 @@ Eval Results (${data._evals.length}):`);
|
|
|
959
963
|
|
|
960
964
|
// src/commands/datasets.ts
|
|
961
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"]);
|
|
962
967
|
function formatBytes(bytes) {
|
|
963
968
|
if (bytes < 1024) return `${bytes} B`;
|
|
964
969
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -1012,10 +1017,85 @@ function findControlChar(value) {
|
|
|
1012
1017
|
function formatCodepoint(code) {
|
|
1013
1018
|
return `U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
1014
1019
|
}
|
|
1015
|
-
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) {
|
|
1016
1095
|
const lines = readFileSync4(file, "utf8").split(/\r?\n/);
|
|
1017
1096
|
const errors = [];
|
|
1018
1097
|
let rowCount = 0;
|
|
1098
|
+
let detectedFormat = null;
|
|
1019
1099
|
for (const [index, rawLine] of lines.entries()) {
|
|
1020
1100
|
const line = rawLine.trim();
|
|
1021
1101
|
if (!line) continue;
|
|
@@ -1039,6 +1119,26 @@ function validateDatasetFile(file) {
|
|
|
1039
1119
|
);
|
|
1040
1120
|
continue;
|
|
1041
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
|
+
}
|
|
1042
1142
|
if (typeof record.input !== "string") {
|
|
1043
1143
|
errors.push(`Row ${rowNumber}: missing string "input" field`);
|
|
1044
1144
|
}
|
|
@@ -1064,10 +1164,11 @@ function validateDatasetFile(file) {
|
|
|
1064
1164
|
const suffix = errors.length > 5 ? `
|
|
1065
1165
|
...and ${errors.length - 5} more error(s)` : "";
|
|
1066
1166
|
throw new Error(
|
|
1067
|
-
`Invalid dataset format. Each JSONL row must be
|
|
1167
|
+
`Invalid dataset format. Each JSONL row must be a valid text or document OCR example.
|
|
1068
1168
|
${preview}${suffix}`
|
|
1069
1169
|
);
|
|
1070
1170
|
}
|
|
1171
|
+
return detectedFormat ?? requestedFormat ?? "jsonl";
|
|
1071
1172
|
}
|
|
1072
1173
|
function registerDatasetsCommands(parent) {
|
|
1073
1174
|
const datasets = parent.command("datasets").description("Manage datasets");
|
|
@@ -1115,13 +1216,13 @@ function registerDatasetsCommands(parent) {
|
|
|
1115
1216
|
}
|
|
1116
1217
|
}
|
|
1117
1218
|
});
|
|
1118
|
-
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) => {
|
|
1119
1220
|
const opts = parent.opts();
|
|
1120
1221
|
if (!existsSync2(file)) {
|
|
1121
1222
|
printError(`File not found: ${file}`);
|
|
1122
1223
|
process.exit(1);
|
|
1123
1224
|
}
|
|
1124
|
-
validateDatasetFile(file);
|
|
1225
|
+
const format = validateDatasetFile(file, parseDatasetFormat(cmdOpts.format));
|
|
1125
1226
|
const name = cmdOpts.name || file.split("/").pop().replace(/\.jsonl?$/, "");
|
|
1126
1227
|
const fileBytes = readFileSync4(file);
|
|
1127
1228
|
const uploadUrl = await post(
|
|
@@ -1131,7 +1232,8 @@ function registerDatasetsCommands(parent) {
|
|
|
1131
1232
|
description: cmdOpts.description ?? null,
|
|
1132
1233
|
filename: file.split("/").pop(),
|
|
1133
1234
|
size: statSync2(file).size,
|
|
1134
|
-
contentType: "application/jsonl"
|
|
1235
|
+
contentType: "application/jsonl",
|
|
1236
|
+
format
|
|
1135
1237
|
},
|
|
1136
1238
|
opts
|
|
1137
1239
|
);
|
|
@@ -2074,7 +2176,10 @@ import json
|
|
|
2074
2176
|
import os
|
|
2075
2177
|
import time
|
|
2076
2178
|
import uuid
|
|
2179
|
+
import base64
|
|
2180
|
+
from io import BytesIO
|
|
2077
2181
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
2182
|
+
from urllib.request import urlopen
|
|
2078
2183
|
|
|
2079
2184
|
MODEL_PATH = os.environ["TT_MODEL_PATH"]
|
|
2080
2185
|
MODEL_NAME = os.environ.get("TT_MODEL_NAME", os.path.basename(MODEL_PATH.rstrip("/")) or "tuned-tensor-model")
|
|
@@ -2125,9 +2230,9 @@ def load_model():
|
|
|
2125
2230
|
) from exc
|
|
2126
2231
|
|
|
2127
2232
|
try:
|
|
2128
|
-
processor = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=TRUST_REMOTE_CODE)
|
|
2129
|
-
except Exception:
|
|
2130
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)
|
|
2131
2236
|
tokenizer = getattr(processor, "tokenizer", processor)
|
|
2132
2237
|
if getattr(tokenizer, "pad_token", None) is None:
|
|
2133
2238
|
tokenizer.pad_token = tokenizer.eos_token
|
|
@@ -2144,22 +2249,43 @@ def load_model():
|
|
|
2144
2249
|
kwargs["torch_dtype"] = torch.float16
|
|
2145
2250
|
|
|
2146
2251
|
try:
|
|
2147
|
-
model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, **kwargs)
|
|
2148
|
-
except Exception:
|
|
2149
2252
|
model = AutoModelForImageTextToText.from_pretrained(MODEL_PATH, **kwargs)
|
|
2253
|
+
except Exception:
|
|
2254
|
+
model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, **kwargs)
|
|
2150
2255
|
if device != "cuda":
|
|
2151
2256
|
model.to(device)
|
|
2152
2257
|
model.eval()
|
|
2153
|
-
return torch, tokenizer, model, device
|
|
2258
|
+
return torch, processor, tokenizer, model, device
|
|
2259
|
+
|
|
2260
|
+
|
|
2261
|
+
torch, processor, tokenizer, model, DEVICE = load_model()
|
|
2154
2262
|
|
|
2155
2263
|
|
|
2156
|
-
|
|
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)}
|
|
2157
2281
|
|
|
2158
2282
|
|
|
2159
|
-
def
|
|
2283
|
+
def normalize_content(content):
|
|
2160
2284
|
if isinstance(content, str):
|
|
2161
2285
|
return content
|
|
2162
|
-
|
|
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 ""
|
|
2163
2289
|
|
|
2164
2290
|
|
|
2165
2291
|
def normalize_messages(raw_messages):
|
|
@@ -2170,7 +2296,7 @@ def normalize_messages(raw_messages):
|
|
|
2170
2296
|
role = item.get("role")
|
|
2171
2297
|
if role not in {"system", "user", "assistant", "tool"}:
|
|
2172
2298
|
continue
|
|
2173
|
-
messages.append({"role": role, "content":
|
|
2299
|
+
messages.append({"role": role, "content": normalize_content(item.get("content", ""))})
|
|
2174
2300
|
|
|
2175
2301
|
if SYSTEM_PROMPT and not any(message["role"] == "system" for message in messages):
|
|
2176
2302
|
messages.insert(0, {"role": "system", "content": SYSTEM_PROMPT})
|
|
@@ -2214,7 +2340,10 @@ def with_json_contract(messages, mode, schema):
|
|
|
2214
2340
|
contracted = [dict(message) for message in messages]
|
|
2215
2341
|
for message in contracted:
|
|
2216
2342
|
if message["role"] == "system":
|
|
2217
|
-
|
|
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()
|
|
2218
2347
|
return contracted
|
|
2219
2348
|
|
|
2220
2349
|
contracted.insert(0, {"role": "system", "content": contract})
|
|
@@ -2233,13 +2362,71 @@ def render_prompt(messages):
|
|
|
2233
2362
|
return "\n".join(rendered)
|
|
2234
2363
|
|
|
2235
2364
|
|
|
2236
|
-
def
|
|
2237
|
-
|
|
2238
|
-
|
|
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)
|
|
2239
2416
|
input_device = getattr(model, "device", None)
|
|
2240
2417
|
if input_device is None:
|
|
2241
2418
|
input_device = next(model.parameters()).device
|
|
2242
|
-
|
|
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)
|
|
2243
2430
|
|
|
2244
2431
|
generate_kwargs = {
|
|
2245
2432
|
**inputs,
|
|
@@ -2667,7 +2854,7 @@ function ensureServingRuntime(python, cacheDir) {
|
|
|
2667
2854
|
const check = `
|
|
2668
2855
|
import importlib.util
|
|
2669
2856
|
import json
|
|
2670
|
-
required = ["torch", "transformers", "accelerate", "safetensors"]
|
|
2857
|
+
required = ["torch", "transformers", "accelerate", "safetensors", "PIL"]
|
|
2671
2858
|
missing = [name for name in required if importlib.util.find_spec(name) is None]
|
|
2672
2859
|
print(json.dumps({"missing": missing}))
|
|
2673
2860
|
`;
|
|
@@ -3238,7 +3425,7 @@ function registerModelsCommands(parent) {
|
|
|
3238
3425
|
const venvDir = runtimeDir(cacheDir);
|
|
3239
3426
|
const venvPython = runtimePythonPath(cacheDir);
|
|
3240
3427
|
const python = pickRuntimePython(cmdOpts.python);
|
|
3241
|
-
const deps = ["torch", "transformers", "accelerate", "safetensors"];
|
|
3428
|
+
const deps = ["torch", "transformers", "accelerate", "safetensors", "pillow"];
|
|
3242
3429
|
const commands = setupRuntimeCommands(python, venvDir, deps);
|
|
3243
3430
|
if (cmdOpts.printCommand) {
|
|
3244
3431
|
const commandLines = commands.map((cmd) => cmd.map(quoteCommandPart).join(" "));
|
|
@@ -3426,6 +3613,14 @@ function registerBalanceCommands(parent) {
|
|
|
3426
3613
|
const balanceLine = lowBalance ? chalk3.red(formatCents3(balance.balance_cents)) + chalk3.dim(" (low)") : chalk3.bold.green(formatCents3(balance.balance_cents));
|
|
3427
3614
|
printDetail([
|
|
3428
3615
|
["Credits", balanceLine],
|
|
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
|
+
],
|
|
3429
3624
|
["Lifetime top-ups", formatCents3(balance.lifetime_topup_cents)]
|
|
3430
3625
|
]);
|
|
3431
3626
|
if (transactions && transactions.length > 0) {
|
|
@@ -3768,7 +3963,7 @@ function registerPushCommand(parent) {
|
|
|
3768
3963
|
|
|
3769
3964
|
// src/index.ts
|
|
3770
3965
|
var program = new Command();
|
|
3771
|
-
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(
|
|
3772
3967
|
"-u, --base-url <url>",
|
|
3773
3968
|
"API base URL (default: https://tunedtensor.com)"
|
|
3774
3969
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|