@tuned-tensor/cli 0.4.11 → 0.4.12
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 +19 -1
- package/dist/index.js +686 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1004,17 +1004,374 @@ function registerDatasetsCommands(parent) {
|
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
1006
|
// src/commands/models.ts
|
|
1007
|
-
import {
|
|
1008
|
-
import {
|
|
1009
|
-
import {
|
|
1007
|
+
import { spawn, execFileSync } from "child_process";
|
|
1008
|
+
import { createHash } from "crypto";
|
|
1009
|
+
import {
|
|
1010
|
+
existsSync as existsSync4,
|
|
1011
|
+
mkdirSync as mkdirSync2,
|
|
1012
|
+
statSync as statSync3,
|
|
1013
|
+
createWriteStream,
|
|
1014
|
+
unlinkSync,
|
|
1015
|
+
readFileSync as readFileSync6,
|
|
1016
|
+
writeFileSync as writeFileSync3,
|
|
1017
|
+
readdirSync,
|
|
1018
|
+
rmSync
|
|
1019
|
+
} from "fs";
|
|
1020
|
+
import { homedir as homedir2 } from "os";
|
|
1021
|
+
import { dirname, join as join2, resolve as resolve2, basename as basename2 } from "path";
|
|
1022
|
+
import { Readable, Transform } from "stream";
|
|
1010
1023
|
import { pipeline } from "stream/promises";
|
|
1024
|
+
|
|
1025
|
+
// src/commands/init.ts
|
|
1026
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1027
|
+
import { resolve } from "path";
|
|
1028
|
+
var DEFAULT_SPEC_FILE = "tunedtensor.json";
|
|
1029
|
+
var SCAFFOLD = {
|
|
1030
|
+
name: "My Agent",
|
|
1031
|
+
description: "",
|
|
1032
|
+
base_model: "Qwen/Qwen3.5-2B",
|
|
1033
|
+
system_prompt: "You are a helpful assistant.",
|
|
1034
|
+
guidelines: [],
|
|
1035
|
+
constraints: [],
|
|
1036
|
+
examples: [
|
|
1037
|
+
{ input: "Hello", output: "Hi! How can I help you today?" }
|
|
1038
|
+
],
|
|
1039
|
+
eval_cases: []
|
|
1040
|
+
};
|
|
1041
|
+
function registerInitCommand(parent) {
|
|
1042
|
+
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) => {
|
|
1043
|
+
const filePath = resolve(cmdOpts.file);
|
|
1044
|
+
if (existsSync3(filePath)) {
|
|
1045
|
+
printWarning(`${cmdOpts.file} already exists. Use tt eval to validate it or edit it directly.`);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const spec = { ...SCAFFOLD };
|
|
1049
|
+
if (cmdOpts.name) spec.name = cmdOpts.name;
|
|
1050
|
+
if (cmdOpts.model) spec.base_model = canonicalizeBaseModel(cmdOpts.model);
|
|
1051
|
+
writeFileSync2(filePath, JSON.stringify(spec, null, 2) + "\n");
|
|
1052
|
+
printSuccess(`Created ${cmdOpts.file}`);
|
|
1053
|
+
console.log("\nNext steps:");
|
|
1054
|
+
console.log(" 1. Edit the spec: system_prompt, guidelines, examples");
|
|
1055
|
+
console.log(" 2. Validate spec: tt eval");
|
|
1056
|
+
console.log(" 3. Push to remote: tt push");
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
function loadSpec(filePath) {
|
|
1060
|
+
const resolved = resolve(filePath || DEFAULT_SPEC_FILE);
|
|
1061
|
+
if (!existsSync3(resolved)) {
|
|
1062
|
+
printError(
|
|
1063
|
+
`Spec file not found: ${filePath || DEFAULT_SPEC_FILE}
|
|
1064
|
+
Run \`tt init\` to create one.`
|
|
1065
|
+
);
|
|
1066
|
+
process.exit(1);
|
|
1067
|
+
}
|
|
1068
|
+
const raw = JSON.parse(readFileSync5(resolved, "utf-8"));
|
|
1069
|
+
return raw;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/commands/models.ts
|
|
1073
|
+
var REFERENCE_SERVER_SCRIPT = String.raw`#!/usr/bin/env python3
|
|
1074
|
+
import json
|
|
1075
|
+
import os
|
|
1076
|
+
import time
|
|
1077
|
+
import uuid
|
|
1078
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
1079
|
+
|
|
1080
|
+
MODEL_PATH = os.environ["TT_MODEL_PATH"]
|
|
1081
|
+
MODEL_NAME = os.environ.get("TT_MODEL_NAME", os.path.basename(MODEL_PATH.rstrip("/")) or "tuned-tensor-model")
|
|
1082
|
+
SYSTEM_PROMPT = os.environ.get("TT_SYSTEM_PROMPT", "").strip()
|
|
1083
|
+
HOST = os.environ.get("TT_HOST", "127.0.0.1")
|
|
1084
|
+
PORT = int(os.environ.get("TT_PORT", "8000"))
|
|
1085
|
+
DEFAULT_MAX_TOKENS = int(os.environ.get("TT_MAX_TOKENS", "512"))
|
|
1086
|
+
DEFAULT_TEMPERATURE = float(os.environ.get("TT_TEMPERATURE", "0.7"))
|
|
1087
|
+
TRUST_REMOTE_CODE = os.environ.get("TT_TRUST_REMOTE_CODE", "false").lower() in {"1", "true", "yes", "y"}
|
|
1088
|
+
REQUESTED_DEVICE = os.environ.get("TT_DEVICE", "auto").lower()
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def choose_device(torch):
|
|
1092
|
+
if REQUESTED_DEVICE not in {"auto", "cpu", "cuda", "mps"}:
|
|
1093
|
+
raise RuntimeError("TT_DEVICE must be one of: auto, cpu, cuda, mps")
|
|
1094
|
+
if REQUESTED_DEVICE == "cuda" and not torch.cuda.is_available():
|
|
1095
|
+
raise RuntimeError("TT_DEVICE=cuda requested, but CUDA is not available")
|
|
1096
|
+
if REQUESTED_DEVICE == "mps" and not (
|
|
1097
|
+
hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
|
|
1098
|
+
):
|
|
1099
|
+
raise RuntimeError("TT_DEVICE=mps requested, but Apple MPS is not available")
|
|
1100
|
+
if REQUESTED_DEVICE != "auto":
|
|
1101
|
+
return REQUESTED_DEVICE
|
|
1102
|
+
if torch.cuda.is_available():
|
|
1103
|
+
return "cuda"
|
|
1104
|
+
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
1105
|
+
return "mps"
|
|
1106
|
+
return "cpu"
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def load_model():
|
|
1110
|
+
try:
|
|
1111
|
+
import torch
|
|
1112
|
+
from transformers import (
|
|
1113
|
+
AutoModelForCausalLM,
|
|
1114
|
+
AutoModelForImageTextToText,
|
|
1115
|
+
AutoProcessor,
|
|
1116
|
+
AutoTokenizer,
|
|
1117
|
+
)
|
|
1118
|
+
except Exception as exc:
|
|
1119
|
+
raise RuntimeError(
|
|
1120
|
+
"Missing Python serving dependencies. Install them with: "
|
|
1121
|
+
"python -m pip install torch transformers accelerate"
|
|
1122
|
+
) from exc
|
|
1123
|
+
|
|
1124
|
+
try:
|
|
1125
|
+
processor = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=TRUST_REMOTE_CODE)
|
|
1126
|
+
except Exception:
|
|
1127
|
+
processor = AutoProcessor.from_pretrained(MODEL_PATH, trust_remote_code=TRUST_REMOTE_CODE)
|
|
1128
|
+
tokenizer = getattr(processor, "tokenizer", processor)
|
|
1129
|
+
if getattr(tokenizer, "pad_token", None) is None:
|
|
1130
|
+
tokenizer.pad_token = tokenizer.eos_token
|
|
1131
|
+
|
|
1132
|
+
device = choose_device(torch)
|
|
1133
|
+
kwargs = {
|
|
1134
|
+
"trust_remote_code": TRUST_REMOTE_CODE,
|
|
1135
|
+
"torch_dtype": "auto",
|
|
1136
|
+
"low_cpu_mem_usage": True,
|
|
1137
|
+
}
|
|
1138
|
+
if device == "cuda":
|
|
1139
|
+
kwargs["device_map"] = "auto"
|
|
1140
|
+
elif device == "mps":
|
|
1141
|
+
kwargs["torch_dtype"] = torch.float16
|
|
1142
|
+
|
|
1143
|
+
try:
|
|
1144
|
+
model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, **kwargs)
|
|
1145
|
+
except Exception:
|
|
1146
|
+
model = AutoModelForImageTextToText.from_pretrained(MODEL_PATH, **kwargs)
|
|
1147
|
+
if device != "cuda":
|
|
1148
|
+
model.to(device)
|
|
1149
|
+
model.eval()
|
|
1150
|
+
return torch, tokenizer, model, device
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
torch, tokenizer, model, DEVICE = load_model()
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def content_to_text(content):
|
|
1157
|
+
if isinstance(content, str):
|
|
1158
|
+
return content
|
|
1159
|
+
return json.dumps(content, ensure_ascii=False)
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def normalize_messages(raw_messages):
|
|
1163
|
+
messages = []
|
|
1164
|
+
for item in raw_messages:
|
|
1165
|
+
if not isinstance(item, dict):
|
|
1166
|
+
continue
|
|
1167
|
+
role = item.get("role")
|
|
1168
|
+
if role not in {"system", "user", "assistant", "tool"}:
|
|
1169
|
+
continue
|
|
1170
|
+
messages.append({"role": role, "content": content_to_text(item.get("content", ""))})
|
|
1171
|
+
|
|
1172
|
+
if SYSTEM_PROMPT and not any(message["role"] == "system" for message in messages):
|
|
1173
|
+
messages.insert(0, {"role": "system", "content": SYSTEM_PROMPT})
|
|
1174
|
+
return messages
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def render_prompt(messages):
|
|
1178
|
+
chat_template = getattr(tokenizer, "chat_template", None)
|
|
1179
|
+
if hasattr(tokenizer, "apply_chat_template") and chat_template:
|
|
1180
|
+
return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
|
1181
|
+
|
|
1182
|
+
rendered = []
|
|
1183
|
+
for message in messages:
|
|
1184
|
+
rendered.append(f"{message['role'].upper()}: {message['content']}")
|
|
1185
|
+
rendered.append("ASSISTANT:")
|
|
1186
|
+
return "\n".join(rendered)
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def generate_completion(messages, max_tokens, temperature):
|
|
1190
|
+
prompt = render_prompt(messages)
|
|
1191
|
+
inputs = tokenizer(prompt, return_tensors="pt")
|
|
1192
|
+
input_device = getattr(model, "device", None)
|
|
1193
|
+
if input_device is None:
|
|
1194
|
+
input_device = next(model.parameters()).device
|
|
1195
|
+
inputs = {key: value.to(input_device) for key, value in inputs.items()}
|
|
1196
|
+
|
|
1197
|
+
generate_kwargs = {
|
|
1198
|
+
**inputs,
|
|
1199
|
+
"max_new_tokens": max_tokens,
|
|
1200
|
+
"pad_token_id": tokenizer.pad_token_id or tokenizer.eos_token_id,
|
|
1201
|
+
}
|
|
1202
|
+
if temperature > 0:
|
|
1203
|
+
generate_kwargs["do_sample"] = True
|
|
1204
|
+
generate_kwargs["temperature"] = temperature
|
|
1205
|
+
else:
|
|
1206
|
+
generate_kwargs["do_sample"] = False
|
|
1207
|
+
|
|
1208
|
+
with torch.no_grad():
|
|
1209
|
+
output_ids = model.generate(**generate_kwargs)
|
|
1210
|
+
|
|
1211
|
+
prompt_tokens = int(inputs["input_ids"].shape[-1])
|
|
1212
|
+
completion_ids = output_ids[0][prompt_tokens:]
|
|
1213
|
+
content = tokenizer.decode(completion_ids, skip_special_tokens=True).strip()
|
|
1214
|
+
return content, prompt_tokens, int(completion_ids.shape[-1])
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
class Handler(BaseHTTPRequestHandler):
|
|
1218
|
+
server_version = "TunedTensorReferenceServer/0.1"
|
|
1219
|
+
|
|
1220
|
+
def send_json(self, status, payload):
|
|
1221
|
+
body = json.dumps(payload).encode("utf-8")
|
|
1222
|
+
self.send_response(status)
|
|
1223
|
+
self.send_header("Content-Type", "application/json")
|
|
1224
|
+
self.send_header("Content-Length", str(len(body)))
|
|
1225
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
1226
|
+
self.send_header("Access-Control-Allow-Headers", "authorization, content-type")
|
|
1227
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
1228
|
+
self.end_headers()
|
|
1229
|
+
self.wfile.write(body)
|
|
1230
|
+
|
|
1231
|
+
def do_OPTIONS(self):
|
|
1232
|
+
self.send_json(204, {})
|
|
1233
|
+
|
|
1234
|
+
def do_GET(self):
|
|
1235
|
+
if self.path == "/health":
|
|
1236
|
+
self.send_json(200, {
|
|
1237
|
+
"status": "ok",
|
|
1238
|
+
"model": MODEL_NAME,
|
|
1239
|
+
"spec_prompt": bool(SYSTEM_PROMPT),
|
|
1240
|
+
"device": DEVICE,
|
|
1241
|
+
})
|
|
1242
|
+
return
|
|
1243
|
+
self.send_json(404, {"error": {"message": "Not found"}})
|
|
1244
|
+
|
|
1245
|
+
def do_POST(self):
|
|
1246
|
+
if self.path not in {"/v1/chat/completions", "/chat/completions"}:
|
|
1247
|
+
self.send_json(404, {"error": {"message": "Not found"}})
|
|
1248
|
+
return
|
|
1249
|
+
|
|
1250
|
+
try:
|
|
1251
|
+
length = int(self.headers.get("content-length", "0"))
|
|
1252
|
+
body = json.loads(self.rfile.read(length) or b"{}")
|
|
1253
|
+
if body.get("stream"):
|
|
1254
|
+
self.send_json(400, {"error": {"message": "Streaming is not supported by tt models serve yet."}})
|
|
1255
|
+
return
|
|
1256
|
+
|
|
1257
|
+
raw_messages = body.get("messages")
|
|
1258
|
+
if not isinstance(raw_messages, list) or not raw_messages:
|
|
1259
|
+
self.send_json(400, {"error": {"message": "Request must include a non-empty messages array."}})
|
|
1260
|
+
return
|
|
1261
|
+
|
|
1262
|
+
max_tokens = int(body.get("max_tokens") or DEFAULT_MAX_TOKENS)
|
|
1263
|
+
temperature = float(body.get("temperature") if body.get("temperature") is not None else DEFAULT_TEMPERATURE)
|
|
1264
|
+
messages = normalize_messages(raw_messages)
|
|
1265
|
+
content, prompt_tokens, completion_tokens = generate_completion(messages, max_tokens, temperature)
|
|
1266
|
+
|
|
1267
|
+
self.send_json(200, {
|
|
1268
|
+
"id": "chatcmpl-" + uuid.uuid4().hex,
|
|
1269
|
+
"object": "chat.completion",
|
|
1270
|
+
"created": int(time.time()),
|
|
1271
|
+
"model": body.get("model") or MODEL_NAME,
|
|
1272
|
+
"choices": [{
|
|
1273
|
+
"index": 0,
|
|
1274
|
+
"message": {"role": "assistant", "content": content},
|
|
1275
|
+
"finish_reason": "stop",
|
|
1276
|
+
}],
|
|
1277
|
+
"usage": {
|
|
1278
|
+
"prompt_tokens": prompt_tokens,
|
|
1279
|
+
"completion_tokens": completion_tokens,
|
|
1280
|
+
"total_tokens": prompt_tokens + completion_tokens,
|
|
1281
|
+
},
|
|
1282
|
+
})
|
|
1283
|
+
except Exception as exc:
|
|
1284
|
+
self.send_json(500, {"error": {"message": str(exc)}})
|
|
1285
|
+
|
|
1286
|
+
def log_message(self, fmt, *args):
|
|
1287
|
+
print("%s - %s" % (self.address_string(), fmt % args))
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def main():
|
|
1291
|
+
print(f"Serving {MODEL_NAME} from {MODEL_PATH}")
|
|
1292
|
+
print(f"Listening on http://{HOST}:{PORT}/v1/chat/completions")
|
|
1293
|
+
if SYSTEM_PROMPT:
|
|
1294
|
+
print("Behavior spec prompt: enabled")
|
|
1295
|
+
else:
|
|
1296
|
+
print("Behavior spec prompt: disabled")
|
|
1297
|
+
print(f"Device: {DEVICE}")
|
|
1298
|
+
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
if __name__ == "__main__":
|
|
1302
|
+
main()
|
|
1303
|
+
`;
|
|
1011
1304
|
function resolveOutputPath(output, filename) {
|
|
1012
1305
|
if (!output) return filename;
|
|
1013
|
-
if (
|
|
1306
|
+
if (existsSync4(output) && statSync3(output).isDirectory()) {
|
|
1014
1307
|
return join2(output, filename);
|
|
1015
1308
|
}
|
|
1016
1309
|
return output;
|
|
1017
1310
|
}
|
|
1311
|
+
function parseContentLength(value) {
|
|
1312
|
+
if (!value) return null;
|
|
1313
|
+
const parsed = Number(value);
|
|
1314
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
1315
|
+
}
|
|
1316
|
+
function formatBytes2(bytes) {
|
|
1317
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
1318
|
+
let value = bytes;
|
|
1319
|
+
let unit = 0;
|
|
1320
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
1321
|
+
value /= 1024;
|
|
1322
|
+
unit += 1;
|
|
1323
|
+
}
|
|
1324
|
+
const precision = unit === 0 || value >= 10 ? 0 : 1;
|
|
1325
|
+
return `${value.toFixed(precision)} ${units[unit]}`;
|
|
1326
|
+
}
|
|
1327
|
+
function formatDuration(seconds) {
|
|
1328
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "--";
|
|
1329
|
+
const rounded = Math.max(1, Math.round(seconds));
|
|
1330
|
+
const hours = Math.floor(rounded / 3600);
|
|
1331
|
+
const minutes = Math.floor(rounded % 3600 / 60);
|
|
1332
|
+
const secs = rounded % 60;
|
|
1333
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
1334
|
+
if (minutes > 0) return `${minutes}m ${secs}s`;
|
|
1335
|
+
return `${secs}s`;
|
|
1336
|
+
}
|
|
1337
|
+
function chunkLength(chunk) {
|
|
1338
|
+
if (typeof chunk === "string") return Buffer.byteLength(chunk);
|
|
1339
|
+
if (chunk instanceof Uint8Array) return chunk.byteLength;
|
|
1340
|
+
return Buffer.byteLength(String(chunk));
|
|
1341
|
+
}
|
|
1342
|
+
function createDownloadProgress(totalBytes, enabled) {
|
|
1343
|
+
const stream = process.stderr;
|
|
1344
|
+
const start = Date.now();
|
|
1345
|
+
let lastRender = 0;
|
|
1346
|
+
let rendered = false;
|
|
1347
|
+
function render(downloadedBytes, force = false) {
|
|
1348
|
+
if (!enabled) return;
|
|
1349
|
+
const now = Date.now();
|
|
1350
|
+
if (!force && now - lastRender < 100 && downloadedBytes !== totalBytes) return;
|
|
1351
|
+
lastRender = now;
|
|
1352
|
+
rendered = true;
|
|
1353
|
+
const elapsedSeconds = Math.max((now - start) / 1e3, 1e-3);
|
|
1354
|
+
const rate = downloadedBytes / elapsedSeconds;
|
|
1355
|
+
const rateText = `${formatBytes2(rate)}/s`;
|
|
1356
|
+
let line;
|
|
1357
|
+
if (totalBytes != null && totalBytes > 0) {
|
|
1358
|
+
const ratio = Math.min(downloadedBytes / totalBytes, 1);
|
|
1359
|
+
const barWidth = 24;
|
|
1360
|
+
const filled = Math.round(ratio * barWidth);
|
|
1361
|
+
const bar = "#".repeat(filled) + "-".repeat(barWidth - filled);
|
|
1362
|
+
const etaSeconds = rate > 0 ? (totalBytes - downloadedBytes) / rate : Number.NaN;
|
|
1363
|
+
line = `Downloading [${bar}] ${(ratio * 100).toFixed(1).padStart(5)}% ${formatBytes2(downloadedBytes)}/${formatBytes2(totalBytes)} ${rateText} ETA ${formatDuration(etaSeconds)}`;
|
|
1364
|
+
} else {
|
|
1365
|
+
line = `Downloading ${formatBytes2(downloadedBytes)} ${rateText} elapsed ${formatDuration(elapsedSeconds)}`;
|
|
1366
|
+
}
|
|
1367
|
+
const columns = stream.columns || 120;
|
|
1368
|
+
stream.write(`\r\x1B[K${line.slice(0, Math.max(columns - 1, 0))}`);
|
|
1369
|
+
}
|
|
1370
|
+
function stop() {
|
|
1371
|
+
if (enabled && rendered) stream.write("\n");
|
|
1372
|
+
}
|
|
1373
|
+
return { render, stop };
|
|
1374
|
+
}
|
|
1018
1375
|
async function downloadUrlToFile(url, outputPath) {
|
|
1019
1376
|
const res = await fetch(url);
|
|
1020
1377
|
if (!res.ok) {
|
|
@@ -1025,17 +1382,225 @@ async function downloadUrlToFile(url, outputPath) {
|
|
|
1025
1382
|
}
|
|
1026
1383
|
mkdirSync2(dirname(outputPath), { recursive: true });
|
|
1027
1384
|
const file = createWriteStream(outputPath);
|
|
1385
|
+
const contentLength = parseContentLength(res.headers.get("content-length"));
|
|
1386
|
+
const progress = createDownloadProgress(
|
|
1387
|
+
contentLength,
|
|
1388
|
+
!isJsonMode() && process.stderr.isTTY === true
|
|
1389
|
+
);
|
|
1390
|
+
let downloadedBytes = 0;
|
|
1391
|
+
const progressStream = new Transform({
|
|
1392
|
+
transform(chunk, _encoding, callback) {
|
|
1393
|
+
downloadedBytes += chunkLength(chunk);
|
|
1394
|
+
progress.render(downloadedBytes);
|
|
1395
|
+
callback(null, chunk);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1028
1398
|
try {
|
|
1029
|
-
|
|
1399
|
+
progress.render(0, true);
|
|
1400
|
+
await pipeline(Readable.fromWeb(res.body), progressStream, file);
|
|
1401
|
+
progress.render(downloadedBytes, true);
|
|
1402
|
+
progress.stop();
|
|
1030
1403
|
} catch (err) {
|
|
1404
|
+
progress.stop();
|
|
1031
1405
|
try {
|
|
1032
1406
|
unlinkSync(outputPath);
|
|
1033
1407
|
} catch {
|
|
1034
1408
|
}
|
|
1035
1409
|
throw err;
|
|
1036
1410
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1411
|
+
return contentLength;
|
|
1412
|
+
}
|
|
1413
|
+
function defaultCacheDir() {
|
|
1414
|
+
const root = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
|
|
1415
|
+
return join2(root, "tuned-tensor", "models");
|
|
1416
|
+
}
|
|
1417
|
+
function runtimeDir(cacheDir) {
|
|
1418
|
+
return join2(cacheDir, "_runtime");
|
|
1419
|
+
}
|
|
1420
|
+
function runtimePythonPath(cacheDir) {
|
|
1421
|
+
const dir = runtimeDir(cacheDir);
|
|
1422
|
+
return process.platform === "win32" ? join2(dir, "Scripts", "python.exe") : join2(dir, "bin", "python");
|
|
1423
|
+
}
|
|
1424
|
+
function resolveServePython(cacheDir, explicitPython) {
|
|
1425
|
+
if (explicitPython) return explicitPython;
|
|
1426
|
+
const managedPython = runtimePythonPath(cacheDir);
|
|
1427
|
+
return existsSync4(managedPython) ? managedPython : "python3";
|
|
1428
|
+
}
|
|
1429
|
+
function commandExists(command) {
|
|
1430
|
+
try {
|
|
1431
|
+
execFileSync(command, ["--version"], { stdio: "ignore" });
|
|
1432
|
+
return true;
|
|
1433
|
+
} catch {
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function pythonMinorVersion(command) {
|
|
1438
|
+
try {
|
|
1439
|
+
const out = execFileSync(
|
|
1440
|
+
command,
|
|
1441
|
+
["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
|
|
1442
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
1443
|
+
).trim();
|
|
1444
|
+
const [major, minor] = out.split(".").map((part) => Number(part));
|
|
1445
|
+
if (major !== 3 || !Number.isFinite(minor)) return null;
|
|
1446
|
+
return minor;
|
|
1447
|
+
} catch {
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function pickRuntimePython(explicitPython) {
|
|
1452
|
+
const candidates = explicitPython ? [explicitPython] : ["python3.12", "python3.11", "python3.10", "python3"];
|
|
1453
|
+
for (const candidate of candidates) {
|
|
1454
|
+
if (!commandExists(candidate)) continue;
|
|
1455
|
+
const minor = pythonMinorVersion(candidate);
|
|
1456
|
+
if (minor != null && minor >= 10 && minor <= 12) return candidate;
|
|
1457
|
+
if (explicitPython) {
|
|
1458
|
+
throw new Error(
|
|
1459
|
+
`${candidate} is Python 3.${minor ?? "?"}. The local serving runtime requires Python 3.10, 3.11, or 3.12 because torch wheels may not be available for newer versions.`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
throw new Error(
|
|
1464
|
+
"Could not find Python 3.10, 3.11, or 3.12. Install one, then run `tt models setup-runtime --python <path>`."
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
function ensureServingRuntime(python, cacheDir) {
|
|
1468
|
+
const check = `
|
|
1469
|
+
import importlib.util
|
|
1470
|
+
import json
|
|
1471
|
+
required = ["torch", "transformers", "accelerate", "safetensors"]
|
|
1472
|
+
missing = [name for name in required if importlib.util.find_spec(name) is None]
|
|
1473
|
+
print(json.dumps({"missing": missing}))
|
|
1474
|
+
`;
|
|
1475
|
+
let missing;
|
|
1476
|
+
try {
|
|
1477
|
+
const out = execFileSync(python, ["-c", check], {
|
|
1478
|
+
encoding: "utf8",
|
|
1479
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1480
|
+
});
|
|
1481
|
+
missing = JSON.parse(out).missing;
|
|
1482
|
+
} catch (err) {
|
|
1483
|
+
const suffix = python === runtimePythonPath(cacheDir) ? " Re-run `tt models setup-runtime` to repair it." : " Run `tt models setup-runtime` to create an isolated serving runtime.";
|
|
1484
|
+
throw new Error(`Python serving runtime check failed for ${python}.${suffix}`);
|
|
1485
|
+
}
|
|
1486
|
+
if (missing.length > 0) {
|
|
1487
|
+
const managedPython = runtimePythonPath(cacheDir);
|
|
1488
|
+
const installHint = python === managedPython ? "Run `tt models setup-runtime --force` to repair it." : "Run `tt models setup-runtime` first, or pass --python <path> to a Python environment with the serving dependencies installed.";
|
|
1489
|
+
throw new Error(
|
|
1490
|
+
`Python serving runtime is missing: ${missing.join(", ")}. ${installHint}`
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
function setupRuntimeCommands(python, venvDir, deps) {
|
|
1495
|
+
const venvPython = process.platform === "win32" ? join2(venvDir, "Scripts", "python.exe") : join2(venvDir, "bin", "python");
|
|
1496
|
+
return [
|
|
1497
|
+
[python, "-m", "venv", venvDir],
|
|
1498
|
+
[venvPython, "-m", "pip", "install", "--upgrade", "pip"],
|
|
1499
|
+
[venvPython, "-m", "pip", "install", ...deps]
|
|
1500
|
+
];
|
|
1501
|
+
}
|
|
1502
|
+
function safeSegment(value) {
|
|
1503
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "model";
|
|
1504
|
+
}
|
|
1505
|
+
function directoryHasFiles(path) {
|
|
1506
|
+
return existsSync4(path) && statSync3(path).isDirectory() && readdirSync(path).length > 0;
|
|
1507
|
+
}
|
|
1508
|
+
function extractArchive(archivePath, targetDir, force) {
|
|
1509
|
+
if (directoryHasFiles(targetDir) && !force) return targetDir;
|
|
1510
|
+
if (existsSync4(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
1511
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
1512
|
+
execFileSync("tar", ["-xzf", archivePath, "-C", targetDir], { stdio: "inherit" });
|
|
1513
|
+
return targetDir;
|
|
1514
|
+
}
|
|
1515
|
+
function localArchiveCacheDir(archivePath, cacheDir) {
|
|
1516
|
+
const resolved = resolve2(archivePath);
|
|
1517
|
+
const digest = createHash("sha256").update(resolved).digest("hex").slice(0, 12);
|
|
1518
|
+
return join2(cacheDir, `local-${safeSegment(basename2(archivePath))}-${digest}`);
|
|
1519
|
+
}
|
|
1520
|
+
function buildSystemPrompt(spec) {
|
|
1521
|
+
const parts = [];
|
|
1522
|
+
if (spec.system_prompt?.trim()) parts.push(spec.system_prompt.trim());
|
|
1523
|
+
if (Array.isArray(spec.guidelines) && spec.guidelines.length > 0) {
|
|
1524
|
+
parts.push(`Guidelines:
|
|
1525
|
+
${spec.guidelines.map((g) => `- ${g}`).join("\n")}`);
|
|
1526
|
+
}
|
|
1527
|
+
if (Array.isArray(spec.constraints) && spec.constraints.length > 0) {
|
|
1528
|
+
parts.push(`Constraints:
|
|
1529
|
+
${spec.constraints.map((c) => `- ${c}`).join("\n")}`);
|
|
1530
|
+
}
|
|
1531
|
+
return parts.join("\n\n");
|
|
1532
|
+
}
|
|
1533
|
+
function findSpecPath(explicitPath, modelPath) {
|
|
1534
|
+
const candidates = explicitPath ? [explicitPath] : [join2(process.cwd(), DEFAULT_SPEC_FILE), join2(modelPath, DEFAULT_SPEC_FILE)];
|
|
1535
|
+
for (const candidate of candidates) {
|
|
1536
|
+
const resolved = resolve2(candidate);
|
|
1537
|
+
if (existsSync4(resolved) && statSync3(resolved).isFile()) return resolved;
|
|
1538
|
+
}
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
function loadSystemPromptFromSpec(specPath) {
|
|
1542
|
+
const spec = JSON.parse(readFileSync6(specPath, "utf8"));
|
|
1543
|
+
return buildSystemPrompt(spec);
|
|
1544
|
+
}
|
|
1545
|
+
function writeReferenceServerScript(cacheDir) {
|
|
1546
|
+
const scriptDir = join2(cacheDir, "_server");
|
|
1547
|
+
mkdirSync2(scriptDir, { recursive: true });
|
|
1548
|
+
const scriptPath = join2(scriptDir, "openai_reference_server.py");
|
|
1549
|
+
writeFileSync3(scriptPath, REFERENCE_SERVER_SCRIPT, "utf8");
|
|
1550
|
+
return scriptPath;
|
|
1551
|
+
}
|
|
1552
|
+
async function resolveServeTarget(target, opts, cacheDir, forceDownload) {
|
|
1553
|
+
if (existsSync4(target)) {
|
|
1554
|
+
const resolved = resolve2(target);
|
|
1555
|
+
const stats = statSync3(resolved);
|
|
1556
|
+
if (stats.isDirectory()) {
|
|
1557
|
+
return {
|
|
1558
|
+
modelPath: resolved,
|
|
1559
|
+
modelName: basename2(resolved),
|
|
1560
|
+
source: "local-directory"
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
if (stats.isFile() && (resolved.endsWith(".tar.gz") || resolved.endsWith(".tgz"))) {
|
|
1564
|
+
const extractDir2 = join2(localArchiveCacheDir(resolved, cacheDir), "model");
|
|
1565
|
+
return {
|
|
1566
|
+
modelPath: extractArchive(resolved, extractDir2, forceDownload),
|
|
1567
|
+
modelName: basename2(resolved).replace(/\.t(ar\.)?gz$/, ""),
|
|
1568
|
+
source: "local-archive"
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
throw new Error(`Unsupported model path: ${target}. Use a model directory or .tar.gz artifact.`);
|
|
1572
|
+
}
|
|
1573
|
+
const fullId = await resolveModelId(target, opts);
|
|
1574
|
+
const { data: model } = await get(`/models/${fullId}`, void 0, opts);
|
|
1575
|
+
const { data } = await get(`/models/${fullId}/download`, void 0, opts);
|
|
1576
|
+
const modelCacheDir = join2(cacheDir, fullId);
|
|
1577
|
+
const archivePath = join2(modelCacheDir, data.filename);
|
|
1578
|
+
const extractDir = join2(modelCacheDir, "model");
|
|
1579
|
+
if (!existsSync4(archivePath) || forceDownload) {
|
|
1580
|
+
await downloadUrlToFile(data.url, archivePath);
|
|
1581
|
+
}
|
|
1582
|
+
extractArchive(archivePath, extractDir, forceDownload);
|
|
1583
|
+
return {
|
|
1584
|
+
modelPath: extractDir,
|
|
1585
|
+
modelName: model.name || fullId,
|
|
1586
|
+
source: "downloaded-model",
|
|
1587
|
+
modelId: fullId
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function quoteCommandPart(part) {
|
|
1591
|
+
if (/^[a-zA-Z0-9_./:=@-]+$/.test(part)) return part;
|
|
1592
|
+
return `'${part.replace(/'/g, "'\\''")}'`;
|
|
1593
|
+
}
|
|
1594
|
+
function printServeCommand(command, args, env) {
|
|
1595
|
+
const envKeys = Object.keys(env).sort();
|
|
1596
|
+
const commandLine = [command, ...args].map(quoteCommandPart).join(" ");
|
|
1597
|
+
if (isJsonMode()) {
|
|
1598
|
+
return printJson({ command, args, env_keys: envKeys, command_line: commandLine });
|
|
1599
|
+
}
|
|
1600
|
+
printDetail([
|
|
1601
|
+
["Command", commandLine],
|
|
1602
|
+
["Environment", envKeys.join(", ")]
|
|
1603
|
+
]);
|
|
1039
1604
|
}
|
|
1040
1605
|
function registerModelsCommands(parent) {
|
|
1041
1606
|
const models = parent.command("models").description("Manage fine-tuned models");
|
|
@@ -1095,7 +1660,7 @@ function registerModelsCommands(parent) {
|
|
|
1095
1660
|
opts
|
|
1096
1661
|
);
|
|
1097
1662
|
const outputPath = resolveOutputPath(cmdOpts.output, data.filename);
|
|
1098
|
-
if (
|
|
1663
|
+
if (existsSync4(outputPath) && !cmdOpts.force) {
|
|
1099
1664
|
throw new Error(`Output file already exists: ${outputPath}. Use --force to overwrite.`);
|
|
1100
1665
|
}
|
|
1101
1666
|
const bytes = await downloadUrlToFile(data.url, outputPath);
|
|
@@ -1111,6 +1676,113 @@ function registerModelsCommands(parent) {
|
|
|
1111
1676
|
const size = bytes == null ? "" : ` (${bytes} bytes)`;
|
|
1112
1677
|
printSuccess(`Model downloaded to ${outputPath}${size}`);
|
|
1113
1678
|
});
|
|
1679
|
+
models.command("setup-runtime").description("Install an isolated Python runtime for local model serving").option("--python <path>", "Python 3.10-3.12 executable to create the runtime").option("--cache-dir <path>", "Cache directory for the managed runtime").option("-f, --force", "Recreate the runtime if it already exists").option("--print-command", "Print the setup commands without running them").action(async (cmdOpts) => {
|
|
1680
|
+
const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
|
|
1681
|
+
const venvDir = runtimeDir(cacheDir);
|
|
1682
|
+
const venvPython = runtimePythonPath(cacheDir);
|
|
1683
|
+
const python = pickRuntimePython(cmdOpts.python);
|
|
1684
|
+
const deps = ["torch", "transformers", "accelerate", "safetensors"];
|
|
1685
|
+
const commands = setupRuntimeCommands(python, venvDir, deps);
|
|
1686
|
+
if (cmdOpts.printCommand) {
|
|
1687
|
+
const commandLines = commands.map((cmd) => cmd.map(quoteCommandPart).join(" "));
|
|
1688
|
+
if (isJsonMode()) {
|
|
1689
|
+
return printJson({
|
|
1690
|
+
python,
|
|
1691
|
+
runtime_dir: venvDir,
|
|
1692
|
+
runtime_python: venvPython,
|
|
1693
|
+
commands: commandLines
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
printDetail([
|
|
1697
|
+
["Python", python],
|
|
1698
|
+
["Runtime", venvDir],
|
|
1699
|
+
["Commands", commandLines.join("\n")]
|
|
1700
|
+
]);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (existsSync4(venvDir) && cmdOpts.force) {
|
|
1704
|
+
rmSync(venvDir, { recursive: true, force: true });
|
|
1705
|
+
}
|
|
1706
|
+
if (!existsSync4(venvPython)) {
|
|
1707
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
1708
|
+
execFileSync(commands[0][0], commands[0].slice(1), { stdio: "inherit" });
|
|
1709
|
+
}
|
|
1710
|
+
execFileSync(commands[1][0], commands[1].slice(1), { stdio: "inherit" });
|
|
1711
|
+
execFileSync(commands[2][0], commands[2].slice(1), { stdio: "inherit" });
|
|
1712
|
+
ensureServingRuntime(venvPython, cacheDir);
|
|
1713
|
+
if (isJsonMode()) {
|
|
1714
|
+
return printJson({
|
|
1715
|
+
runtime_dir: venvDir,
|
|
1716
|
+
runtime_python: venvPython,
|
|
1717
|
+
installed: true,
|
|
1718
|
+
dependencies: deps
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
printSuccess(`Serving runtime ready at ${venvDir}`);
|
|
1722
|
+
console.log(`Use it with: tt models serve <model-id> --spec ${DEFAULT_SPEC_FILE}`);
|
|
1723
|
+
});
|
|
1724
|
+
models.command("serve").description("Serve a downloaded model with an OpenAI-compatible local API").argument("<target>", "Model ID/prefix, model directory, or .tar.gz artifact").option("--spec <path>", "Behavior spec JSON to apply as the default system prompt").option("--no-spec-prompt", "Do not auto-apply a behavior spec system prompt").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "8000").option("--python <path>", "Python executable").option("--cache-dir <path>", "Cache directory for downloaded/extracted models").option("--device <device>", "Inference device: auto, cpu, cuda, or mps", "auto").option("--force-download", "Re-download and re-extract model artifacts").option("--max-tokens <n>", "Default max completion tokens", "512").option("--temperature <n>", "Default sampling temperature", "0.7").option("--trust-remote-code", "Pass trust_remote_code=True to transformers").option("--print-command", "Print the underlying Python command without starting it").action(async (target, cmdOpts) => {
|
|
1725
|
+
const opts = parent.opts();
|
|
1726
|
+
const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
|
|
1727
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
1728
|
+
const python = resolveServePython(cacheDir, cmdOpts.python);
|
|
1729
|
+
if (!cmdOpts.printCommand) ensureServingRuntime(python, cacheDir);
|
|
1730
|
+
const serveTarget = await resolveServeTarget(
|
|
1731
|
+
target,
|
|
1732
|
+
opts,
|
|
1733
|
+
cacheDir,
|
|
1734
|
+
Boolean(cmdOpts.forceDownload)
|
|
1735
|
+
);
|
|
1736
|
+
const serverScript = writeReferenceServerScript(cacheDir);
|
|
1737
|
+
let specPath = null;
|
|
1738
|
+
let systemPrompt = "";
|
|
1739
|
+
if (cmdOpts.specPrompt !== false) {
|
|
1740
|
+
specPath = findSpecPath(cmdOpts.spec, serveTarget.modelPath);
|
|
1741
|
+
if (cmdOpts.spec && !specPath) {
|
|
1742
|
+
throw new Error(`Spec file not found: ${cmdOpts.spec}`);
|
|
1743
|
+
}
|
|
1744
|
+
if (specPath) {
|
|
1745
|
+
systemPrompt = loadSystemPromptFromSpec(specPath);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
const env = {
|
|
1749
|
+
TT_MODEL_PATH: serveTarget.modelPath,
|
|
1750
|
+
TT_MODEL_NAME: serveTarget.modelName,
|
|
1751
|
+
TT_HOST: cmdOpts.host,
|
|
1752
|
+
TT_PORT: String(cmdOpts.port),
|
|
1753
|
+
TT_MAX_TOKENS: String(cmdOpts.maxTokens),
|
|
1754
|
+
TT_TEMPERATURE: String(cmdOpts.temperature),
|
|
1755
|
+
TT_TRUST_REMOTE_CODE: cmdOpts.trustRemoteCode ? "true" : "false",
|
|
1756
|
+
TT_DEVICE: String(cmdOpts.device)
|
|
1757
|
+
};
|
|
1758
|
+
if (systemPrompt) env.TT_SYSTEM_PROMPT = systemPrompt;
|
|
1759
|
+
const args = [serverScript];
|
|
1760
|
+
if (cmdOpts.printCommand) return printServeCommand(python, args, env);
|
|
1761
|
+
if (!isJsonMode()) {
|
|
1762
|
+
printSuccess(`Serving ${serveTarget.modelName} from ${serveTarget.modelPath}`);
|
|
1763
|
+
if (systemPrompt && specPath) {
|
|
1764
|
+
printSuccess(`Behavior spec prompt loaded from ${specPath}`);
|
|
1765
|
+
} else if (cmdOpts.specPrompt !== false) {
|
|
1766
|
+
printWarning(
|
|
1767
|
+
`No ${DEFAULT_SPEC_FILE} found. Pass --spec <path> to preserve the behavior prompt.`
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
console.log(`OpenAI-compatible endpoint: http://${cmdOpts.host}:${cmdOpts.port}/v1/chat/completions`);
|
|
1771
|
+
console.log(`Health check: http://${cmdOpts.host}:${cmdOpts.port}/health`);
|
|
1772
|
+
console.log(`Python runtime: ${python}`);
|
|
1773
|
+
}
|
|
1774
|
+
await new Promise((resolvePromise, reject) => {
|
|
1775
|
+
const child = spawn(python, args, {
|
|
1776
|
+
stdio: "inherit",
|
|
1777
|
+
env: { ...process.env, ...env }
|
|
1778
|
+
});
|
|
1779
|
+
child.on("error", reject);
|
|
1780
|
+
child.on("exit", (code, signal) => {
|
|
1781
|
+
if (code === 0) return resolvePromise();
|
|
1782
|
+
reject(new Error(`Model server exited with ${signal ?? `code ${code}`}`));
|
|
1783
|
+
});
|
|
1784
|
+
});
|
|
1785
|
+
});
|
|
1114
1786
|
models.command("delete").description("Delete a model").argument("<id>", "Model ID (full UUID or 4+ char prefix)").action(async (id) => {
|
|
1115
1787
|
const opts = parent.opts();
|
|
1116
1788
|
const fullId = await resolveModelId(id, opts);
|
|
@@ -1274,53 +1946,6 @@ function registerTopupCommands(parent) {
|
|
|
1274
1946
|
});
|
|
1275
1947
|
}
|
|
1276
1948
|
|
|
1277
|
-
// src/commands/init.ts
|
|
1278
|
-
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1279
|
-
import { resolve } from "path";
|
|
1280
|
-
var DEFAULT_SPEC_FILE = "tunedtensor.json";
|
|
1281
|
-
var SCAFFOLD = {
|
|
1282
|
-
name: "My Agent",
|
|
1283
|
-
description: "",
|
|
1284
|
-
base_model: "Qwen/Qwen3.5-2B",
|
|
1285
|
-
system_prompt: "You are a helpful assistant.",
|
|
1286
|
-
guidelines: [],
|
|
1287
|
-
constraints: [],
|
|
1288
|
-
examples: [
|
|
1289
|
-
{ input: "Hello", output: "Hi! How can I help you today?" }
|
|
1290
|
-
],
|
|
1291
|
-
eval_cases: []
|
|
1292
|
-
};
|
|
1293
|
-
function registerInitCommand(parent) {
|
|
1294
|
-
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) => {
|
|
1295
|
-
const filePath = resolve(cmdOpts.file);
|
|
1296
|
-
if (existsSync4(filePath)) {
|
|
1297
|
-
printWarning(`${cmdOpts.file} already exists. Use tt eval to validate it or edit it directly.`);
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
const spec = { ...SCAFFOLD };
|
|
1301
|
-
if (cmdOpts.name) spec.name = cmdOpts.name;
|
|
1302
|
-
if (cmdOpts.model) spec.base_model = canonicalizeBaseModel(cmdOpts.model);
|
|
1303
|
-
writeFileSync2(filePath, JSON.stringify(spec, null, 2) + "\n");
|
|
1304
|
-
printSuccess(`Created ${cmdOpts.file}`);
|
|
1305
|
-
console.log("\nNext steps:");
|
|
1306
|
-
console.log(" 1. Edit the spec: system_prompt, guidelines, examples");
|
|
1307
|
-
console.log(" 2. Validate spec: tt eval");
|
|
1308
|
-
console.log(" 3. Push to remote: tt push");
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
function loadSpec(filePath) {
|
|
1312
|
-
const resolved = resolve(filePath || DEFAULT_SPEC_FILE);
|
|
1313
|
-
if (!existsSync4(resolved)) {
|
|
1314
|
-
printError(
|
|
1315
|
-
`Spec file not found: ${filePath || DEFAULT_SPEC_FILE}
|
|
1316
|
-
Run \`tt init\` to create one.`
|
|
1317
|
-
);
|
|
1318
|
-
process.exit(1);
|
|
1319
|
-
}
|
|
1320
|
-
const raw = JSON.parse(readFileSync5(resolved, "utf-8"));
|
|
1321
|
-
return raw;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
1949
|
// src/commands/eval.ts
|
|
1325
1950
|
import chalk5 from "chalk";
|
|
1326
1951
|
|
|
@@ -1432,12 +2057,12 @@ function printValidation(checks) {
|
|
|
1432
2057
|
}
|
|
1433
2058
|
|
|
1434
2059
|
// src/commands/push.ts
|
|
1435
|
-
import { readFileSync as
|
|
1436
|
-
import { resolve as
|
|
2060
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
2061
|
+
import { resolve as resolve3 } from "path";
|
|
1437
2062
|
function registerPushCommand(parent) {
|
|
1438
2063
|
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) => {
|
|
1439
2064
|
const opts = parent.opts();
|
|
1440
|
-
const filePath =
|
|
2065
|
+
const filePath = resolve3(cmdOpts.file);
|
|
1441
2066
|
const spec = loadSpec(cmdOpts.file);
|
|
1442
2067
|
const { id, eval_cases, ...rawBody } = spec;
|
|
1443
2068
|
const body = canonicalizeSpecBaseModel(rawBody);
|
|
@@ -1448,9 +2073,9 @@ function registerPushCommand(parent) {
|
|
|
1448
2073
|
} else {
|
|
1449
2074
|
const res = await post("/behavior-specs", body, opts);
|
|
1450
2075
|
data = res.data;
|
|
1451
|
-
const raw = JSON.parse(
|
|
2076
|
+
const raw = JSON.parse(readFileSync7(filePath, "utf-8"));
|
|
1452
2077
|
raw.id = data.id;
|
|
1453
|
-
|
|
2078
|
+
writeFileSync4(filePath, JSON.stringify(raw, null, 2) + "\n");
|
|
1454
2079
|
}
|
|
1455
2080
|
if (isJsonMode()) return printJson(data);
|
|
1456
2081
|
printSuccess(
|
|
@@ -1461,7 +2086,7 @@ function registerPushCommand(parent) {
|
|
|
1461
2086
|
|
|
1462
2087
|
// src/index.ts
|
|
1463
2088
|
var program = new Command();
|
|
1464
|
-
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.
|
|
2089
|
+
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.12").option("-k, --api-key <key>", "API key (overrides stored key)").option(
|
|
1465
2090
|
"-u, --base-url <url>",
|
|
1466
2091
|
"API base URL (default: https://tunedtensor.com)"
|
|
1467
2092
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|