@tuned-tensor/cli 0.4.14 → 0.4.16
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 +8 -2
- package/dist/index.js +597 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -595,6 +595,13 @@ function formatEvalScore(run) {
|
|
|
595
595
|
const score = getEvalScore(run);
|
|
596
596
|
return score == null ? void 0 : (score * 100).toFixed(1) + "%";
|
|
597
597
|
}
|
|
598
|
+
function formatPercent(rate) {
|
|
599
|
+
return rate == null ? void 0 : (rate * 100).toFixed(1) + "%";
|
|
600
|
+
}
|
|
601
|
+
function formatCountRate(count, total, rate) {
|
|
602
|
+
if (count == null || total == null || rate == null) return void 0;
|
|
603
|
+
return `${formatPercent(rate)} (${count}/${total})`;
|
|
604
|
+
}
|
|
598
605
|
function formatMinutes(minutes) {
|
|
599
606
|
if (minutes == null) return void 0;
|
|
600
607
|
if (minutes < 60) return `${Math.max(1, Math.round(minutes))}m`;
|
|
@@ -632,6 +639,78 @@ function printDiagnostics(diagnostics) {
|
|
|
632
639
|
console.log(` - ${insight}`);
|
|
633
640
|
}
|
|
634
641
|
}
|
|
642
|
+
printEvalOutputDiagnostics(diagnostics.output_diagnostics);
|
|
643
|
+
}
|
|
644
|
+
function printEvalOutputDiagnostics(diagnostics) {
|
|
645
|
+
if (!diagnostics) return;
|
|
646
|
+
const candidate = diagnostics.candidate;
|
|
647
|
+
const baseline = diagnostics.baseline;
|
|
648
|
+
console.log("\nOutput Diagnostics:");
|
|
649
|
+
printDetail([
|
|
650
|
+
["Tuned Valid JSON", formatCountRate(candidate.valid_json_count, candidate.total, candidate.valid_json_rate)],
|
|
651
|
+
["Tuned Strict JSON", formatCountRate(candidate.strict_json_count, candidate.total, candidate.strict_json_rate)],
|
|
652
|
+
[
|
|
653
|
+
"Tuned Schema Keys",
|
|
654
|
+
formatCountRate(candidate.expected_schema_keys_count, candidate.total, candidate.expected_schema_keys_rate)
|
|
655
|
+
],
|
|
656
|
+
[
|
|
657
|
+
"Tuned Non-JSON Prefix",
|
|
658
|
+
formatCountRate(candidate.non_json_prefix_count, candidate.total, candidate.non_json_prefix_rate)
|
|
659
|
+
],
|
|
660
|
+
[
|
|
661
|
+
"Tuned Reasoning Prefix",
|
|
662
|
+
formatCountRate(
|
|
663
|
+
candidate.visible_reasoning_prefix_count,
|
|
664
|
+
candidate.total,
|
|
665
|
+
candidate.visible_reasoning_prefix_rate
|
|
666
|
+
)
|
|
667
|
+
],
|
|
668
|
+
["Tuned Avg Output", candidate.avg_output_chars == null ? void 0 : `${candidate.avg_output_chars} chars`],
|
|
669
|
+
["Base Valid JSON", formatCountRate(baseline.valid_json_count, baseline.total, baseline.valid_json_rate)],
|
|
670
|
+
[
|
|
671
|
+
"Base Reasoning Prefix",
|
|
672
|
+
formatCountRate(
|
|
673
|
+
baseline.visible_reasoning_prefix_count,
|
|
674
|
+
baseline.total,
|
|
675
|
+
baseline.visible_reasoning_prefix_rate
|
|
676
|
+
)
|
|
677
|
+
]
|
|
678
|
+
]);
|
|
679
|
+
if (diagnostics.test) {
|
|
680
|
+
console.log("\nTest Output Diagnostics:");
|
|
681
|
+
printDetail([
|
|
682
|
+
[
|
|
683
|
+
"Tuned Valid JSON",
|
|
684
|
+
formatCountRate(
|
|
685
|
+
diagnostics.test.candidate.valid_json_count,
|
|
686
|
+
diagnostics.test.candidate.total,
|
|
687
|
+
diagnostics.test.candidate.valid_json_rate
|
|
688
|
+
)
|
|
689
|
+
],
|
|
690
|
+
[
|
|
691
|
+
"Tuned Strict JSON",
|
|
692
|
+
formatCountRate(
|
|
693
|
+
diagnostics.test.candidate.strict_json_count,
|
|
694
|
+
diagnostics.test.candidate.total,
|
|
695
|
+
diagnostics.test.candidate.strict_json_rate
|
|
696
|
+
)
|
|
697
|
+
],
|
|
698
|
+
[
|
|
699
|
+
"Tuned Reasoning Prefix",
|
|
700
|
+
formatCountRate(
|
|
701
|
+
diagnostics.test.candidate.visible_reasoning_prefix_count,
|
|
702
|
+
diagnostics.test.candidate.total,
|
|
703
|
+
diagnostics.test.candidate.visible_reasoning_prefix_rate
|
|
704
|
+
)
|
|
705
|
+
]
|
|
706
|
+
]);
|
|
707
|
+
}
|
|
708
|
+
if (diagnostics.insights?.length) {
|
|
709
|
+
console.log("\nEvaluation Insights:");
|
|
710
|
+
for (const insight of diagnostics.insights) {
|
|
711
|
+
console.log(` - ${insight}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
635
714
|
}
|
|
636
715
|
function registerRunsCommands(parent) {
|
|
637
716
|
const runs = parent.command("runs").description("Manage runs");
|
|
@@ -687,6 +766,7 @@ function registerRunsCommands(parent) {
|
|
|
687
766
|
console.log(` ${k}: ${v}`);
|
|
688
767
|
}
|
|
689
768
|
}
|
|
769
|
+
printEvalOutputDiagnostics(data.eval_summary?.output_diagnostics);
|
|
690
770
|
if (data._events?.length) {
|
|
691
771
|
console.log("\nLatest Updates:");
|
|
692
772
|
for (const event of data._events.slice(-5)) {
|
|
@@ -711,11 +791,14 @@ Eval Results (${data._evals.length}):`);
|
|
|
711
791
|
);
|
|
712
792
|
}
|
|
713
793
|
});
|
|
714
|
-
runs.command("start").description("Start a new run for a behaviour spec").argument("<spec-id>", "Behaviour spec ID (full UUID or 8+ char prefix)").option("--no-augment", "Disable data augmentation").option("--no-llm-judge", "Disable LLM judging").option("--epochs <n>", "Number of training epochs").option("--lr <rate>", "Learning rate").option("--batch-size <n>", "Batch size").option("--dataset <id>", "Dataset ID to use instead of inline spec examples (full UUID or 4+ char prefix)").option("--train-ratio <ratio>", "Dataset training split ratio (default: 0.8 when any split ratio is set)").option("--validation-ratio <ratio>", "Dataset validation split ratio (default: 0.1 when any split ratio is set)").option("--test-ratio <ratio>", "Dataset test split ratio (default: 0.1 when any split ratio is set)").option("--lora-rank <n>", "LoRA rank").option("--lora-alpha <n>", "LoRA alpha").option("--max-eval-examples <n>", "Max examples for the primary eval pass").option("--max-test-eval-examples <n>", "Max examples for the secondary test eval pass").action(async (specId, cmdOpts) => {
|
|
794
|
+
runs.command("start").description("Start a new run for a behaviour spec").argument("<spec-id>", "Behaviour spec ID (full UUID or 8+ char prefix)").option("--no-augment", "Disable data augmentation").option("--no-llm-judge", "Disable LLM judging").option("--epochs <n>", "Number of training epochs").option("--lr <rate>", "Learning rate").option("--batch-size <n>", "Batch size").option("--dataset <id>", "Dataset ID to use instead of inline spec examples (full UUID or 4+ char prefix)").option("--parent-model <id>", "Fine-tuned model ID to continue training from (full UUID or 4+ char prefix)").option("--train-ratio <ratio>", "Dataset training split ratio (default: 0.8 when any split ratio is set)").option("--validation-ratio <ratio>", "Dataset validation split ratio (default: 0.1 when any split ratio is set)").option("--test-ratio <ratio>", "Dataset test split ratio (default: 0.1 when any split ratio is set)").option("--lora-rank <n>", "LoRA rank").option("--lora-alpha <n>", "LoRA alpha").option("--max-eval-examples <n>", "Max examples for the primary eval pass").option("--max-test-eval-examples <n>", "Max examples for the secondary test eval pass").action(async (specId, cmdOpts) => {
|
|
715
795
|
const opts = parent.opts();
|
|
716
796
|
const body = {};
|
|
717
797
|
if (cmdOpts.augment === false) body.augment = false;
|
|
718
798
|
if (cmdOpts.dataset) body.dataset_id = await resolveDatasetId(cmdOpts.dataset, opts);
|
|
799
|
+
if (cmdOpts.parentModel) {
|
|
800
|
+
body.parent_model_id = await resolveModelId(cmdOpts.parentModel, opts);
|
|
801
|
+
}
|
|
719
802
|
const splitRatioOptions = [
|
|
720
803
|
cmdOpts.trainRatio,
|
|
721
804
|
cmdOpts.validationRatio,
|
|
@@ -1009,11 +1092,11 @@ function registerDatasetsCommands(parent) {
|
|
|
1009
1092
|
}
|
|
1010
1093
|
|
|
1011
1094
|
// src/commands/models.ts
|
|
1012
|
-
import { spawn, execFileSync } from "child_process";
|
|
1095
|
+
import { spawn as spawn2, execFileSync } from "child_process";
|
|
1013
1096
|
import { createHash } from "crypto";
|
|
1014
1097
|
import {
|
|
1015
1098
|
existsSync as existsSync4,
|
|
1016
|
-
mkdirSync as
|
|
1099
|
+
mkdirSync as mkdirSync3,
|
|
1017
1100
|
statSync as statSync3,
|
|
1018
1101
|
createWriteStream,
|
|
1019
1102
|
unlinkSync,
|
|
@@ -1023,7 +1106,7 @@ import {
|
|
|
1023
1106
|
rmSync
|
|
1024
1107
|
} from "fs";
|
|
1025
1108
|
import { homedir as homedir2 } from "os";
|
|
1026
|
-
import { dirname, join as join2, resolve as resolve2, basename as basename2 } from "path";
|
|
1109
|
+
import { dirname as dirname2, join as join2, resolve as resolve2, basename as basename2 } from "path";
|
|
1027
1110
|
import { Readable, Transform } from "stream";
|
|
1028
1111
|
import { pipeline } from "stream/promises";
|
|
1029
1112
|
|
|
@@ -1074,6 +1157,444 @@ Run \`tt init\` to create one.`
|
|
|
1074
1157
|
return raw;
|
|
1075
1158
|
}
|
|
1076
1159
|
|
|
1160
|
+
// src/serve-manager.ts
|
|
1161
|
+
import { spawn } from "child_process";
|
|
1162
|
+
import { randomUUID } from "crypto";
|
|
1163
|
+
import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1164
|
+
import { createServer } from "http";
|
|
1165
|
+
import { createServer as createNetServer } from "net";
|
|
1166
|
+
import { dirname } from "path";
|
|
1167
|
+
var COMPLETION_ROUTES = /* @__PURE__ */ new Set(["/v1/chat/completions", "/chat/completions"]);
|
|
1168
|
+
function parsePositiveInteger(value, fallback) {
|
|
1169
|
+
return Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
1170
|
+
}
|
|
1171
|
+
function sleep(ms) {
|
|
1172
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1173
|
+
}
|
|
1174
|
+
function readRequestBody(req) {
|
|
1175
|
+
return new Promise((resolve4, reject) => {
|
|
1176
|
+
const chunks = [];
|
|
1177
|
+
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
1178
|
+
req.on("end", () => resolve4(Buffer.concat(chunks)));
|
|
1179
|
+
req.on("error", reject);
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
function sendJson(res, status, payload) {
|
|
1183
|
+
const body = Buffer.from(JSON.stringify(payload), "utf8");
|
|
1184
|
+
res.writeHead(status, {
|
|
1185
|
+
"content-type": "application/json",
|
|
1186
|
+
"content-length": String(body.byteLength),
|
|
1187
|
+
"access-control-allow-origin": "*",
|
|
1188
|
+
"access-control-allow-headers": "authorization, content-type",
|
|
1189
|
+
"access-control-allow-methods": "GET, POST, OPTIONS"
|
|
1190
|
+
});
|
|
1191
|
+
res.end(body);
|
|
1192
|
+
}
|
|
1193
|
+
function copyResponseHeaders(headers) {
|
|
1194
|
+
const copied = {
|
|
1195
|
+
"access-control-allow-origin": "*",
|
|
1196
|
+
"access-control-allow-headers": "authorization, content-type",
|
|
1197
|
+
"access-control-allow-methods": "GET, POST, OPTIONS"
|
|
1198
|
+
};
|
|
1199
|
+
const contentType = headers.get("content-type");
|
|
1200
|
+
if (contentType) copied["content-type"] = contentType;
|
|
1201
|
+
return copied;
|
|
1202
|
+
}
|
|
1203
|
+
function tryJsonParse(value) {
|
|
1204
|
+
try {
|
|
1205
|
+
return JSON.parse(value);
|
|
1206
|
+
} catch {
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function readPath(value, path) {
|
|
1211
|
+
if (!path) return null;
|
|
1212
|
+
let current = value;
|
|
1213
|
+
for (const part of path.split(".")) {
|
|
1214
|
+
if (!part) return null;
|
|
1215
|
+
if (!current || typeof current !== "object" || !(part in current)) return null;
|
|
1216
|
+
current = current[part];
|
|
1217
|
+
}
|
|
1218
|
+
return current ?? null;
|
|
1219
|
+
}
|
|
1220
|
+
function extractAssistantContent(response) {
|
|
1221
|
+
if (!response || typeof response !== "object") return null;
|
|
1222
|
+
const choices = response.choices;
|
|
1223
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
1224
|
+
const first = choices[0];
|
|
1225
|
+
if (!first || typeof first !== "object") return null;
|
|
1226
|
+
const message = first.message;
|
|
1227
|
+
if (!message || typeof message !== "object") return null;
|
|
1228
|
+
const content = message.content;
|
|
1229
|
+
return typeof content === "string" ? content : null;
|
|
1230
|
+
}
|
|
1231
|
+
function usageNumber(response, key) {
|
|
1232
|
+
if (!response || typeof response !== "object") return null;
|
|
1233
|
+
const usage = response.usage;
|
|
1234
|
+
if (!usage || typeof usage !== "object") return null;
|
|
1235
|
+
const value = usage[key];
|
|
1236
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1237
|
+
}
|
|
1238
|
+
function errorSummary(response, fallback) {
|
|
1239
|
+
if (!response || typeof response !== "object") return fallback;
|
|
1240
|
+
const error = response.error;
|
|
1241
|
+
if (!error || typeof error !== "object") return fallback;
|
|
1242
|
+
const message = error.message;
|
|
1243
|
+
return typeof message === "string" ? message : fallback;
|
|
1244
|
+
}
|
|
1245
|
+
async function allocatePort(host) {
|
|
1246
|
+
return new Promise((resolve4, reject) => {
|
|
1247
|
+
const server = createNetServer();
|
|
1248
|
+
server.on("error", reject);
|
|
1249
|
+
server.listen(0, host, () => {
|
|
1250
|
+
const address = server.address();
|
|
1251
|
+
if (!address || typeof address === "string") {
|
|
1252
|
+
server.close(() => reject(new Error("Could not allocate an internal port")));
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const port = address.port;
|
|
1256
|
+
server.close(() => resolve4(port));
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
var ManagedModelServer = class {
|
|
1261
|
+
constructor(options) {
|
|
1262
|
+
this.options = options;
|
|
1263
|
+
this.spawnModelServer = options.spawnModelServer ?? spawn;
|
|
1264
|
+
this.idleTimeoutMs = parsePositiveInteger(options.idleTimeoutSeconds, 300) * 1e3;
|
|
1265
|
+
this.restartAfterRequests = parsePositiveInteger(options.restartAfterRequests, 100);
|
|
1266
|
+
this.logRecordOverride = options.logRecord;
|
|
1267
|
+
}
|
|
1268
|
+
spawnModelServer;
|
|
1269
|
+
idleTimeoutMs;
|
|
1270
|
+
restartAfterRequests;
|
|
1271
|
+
logRecordOverride;
|
|
1272
|
+
server = null;
|
|
1273
|
+
child = null;
|
|
1274
|
+
internalPort = null;
|
|
1275
|
+
queue = Promise.resolve();
|
|
1276
|
+
idleTimer = null;
|
|
1277
|
+
activeRequests = 0;
|
|
1278
|
+
queuedRequests = 0;
|
|
1279
|
+
requestsSinceStart = 0;
|
|
1280
|
+
restartCount = 0;
|
|
1281
|
+
stoppingChild = false;
|
|
1282
|
+
get isModelRunning() {
|
|
1283
|
+
return this.child != null;
|
|
1284
|
+
}
|
|
1285
|
+
get restarts() {
|
|
1286
|
+
return this.restartCount;
|
|
1287
|
+
}
|
|
1288
|
+
async start() {
|
|
1289
|
+
if (this.server) return;
|
|
1290
|
+
this.internalPort = await allocatePort("127.0.0.1");
|
|
1291
|
+
this.server = createServer((req, res) => {
|
|
1292
|
+
this.handle(req, res).catch((err) => {
|
|
1293
|
+
sendJson(res, 500, { error: { message: err instanceof Error ? err.message : String(err) } });
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
await new Promise((resolve4, reject) => {
|
|
1297
|
+
this.server?.once("error", reject);
|
|
1298
|
+
this.server?.listen(this.options.publicPort, this.options.publicHost, () => {
|
|
1299
|
+
this.server?.off("error", reject);
|
|
1300
|
+
resolve4();
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
async stop() {
|
|
1305
|
+
this.clearIdleTimer();
|
|
1306
|
+
await this.stopChild();
|
|
1307
|
+
if (!this.server) return;
|
|
1308
|
+
const server = this.server;
|
|
1309
|
+
this.server = null;
|
|
1310
|
+
await new Promise((resolve4, reject) => {
|
|
1311
|
+
server.close((err) => err ? reject(err) : resolve4());
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
async run() {
|
|
1315
|
+
await this.start();
|
|
1316
|
+
await new Promise((resolve4, reject) => {
|
|
1317
|
+
this.server?.once("error", reject);
|
|
1318
|
+
this.server?.once("close", resolve4);
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
async handle(req, res) {
|
|
1322
|
+
const route = new URL(req.url || "/", "http://localhost").pathname;
|
|
1323
|
+
if (req.method === "OPTIONS") {
|
|
1324
|
+
sendJson(res, 204, {});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (req.method === "GET" && route === "/health") {
|
|
1328
|
+
await this.handleHealth(res);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (req.method === "POST" && COMPLETION_ROUTES.has(route)) {
|
|
1332
|
+
const receivedAt = Date.now();
|
|
1333
|
+
const body = await readRequestBody(req);
|
|
1334
|
+
this.queuedRequests += 1;
|
|
1335
|
+
const work = this.queue.then(() => this.processGeneration(route, body, res, receivedAt));
|
|
1336
|
+
this.queue = work.catch(() => void 0);
|
|
1337
|
+
try {
|
|
1338
|
+
await work;
|
|
1339
|
+
} finally {
|
|
1340
|
+
this.queuedRequests -= 1;
|
|
1341
|
+
this.scheduleIdleStop();
|
|
1342
|
+
}
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
sendJson(res, 404, { error: { message: "Not found" } });
|
|
1346
|
+
}
|
|
1347
|
+
async handleHealth(res) {
|
|
1348
|
+
if (!this.child) {
|
|
1349
|
+
sendJson(res, 200, {
|
|
1350
|
+
status: "ok",
|
|
1351
|
+
wrapper: "ok",
|
|
1352
|
+
model_status: "cold",
|
|
1353
|
+
restart_count: this.restartCount
|
|
1354
|
+
});
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (this.activeRequests > 0 || this.queuedRequests > 0) {
|
|
1358
|
+
sendJson(res, 200, {
|
|
1359
|
+
status: "ok",
|
|
1360
|
+
wrapper: "ok",
|
|
1361
|
+
model_status: "busy",
|
|
1362
|
+
restart_count: this.restartCount
|
|
1363
|
+
});
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const health = await this.checkHealth();
|
|
1367
|
+
if (health.ok) {
|
|
1368
|
+
sendJson(res, 200, {
|
|
1369
|
+
status: "ok",
|
|
1370
|
+
wrapper: "ok",
|
|
1371
|
+
model_status: "warm",
|
|
1372
|
+
restart_count: this.restartCount,
|
|
1373
|
+
model: health.body
|
|
1374
|
+
});
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
await this.restartChild();
|
|
1378
|
+
sendJson(res, 200, {
|
|
1379
|
+
status: "ok",
|
|
1380
|
+
wrapper: "ok",
|
|
1381
|
+
model_status: "restarted",
|
|
1382
|
+
restart_count: this.restartCount
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
async processGeneration(route, body, res, receivedAt) {
|
|
1386
|
+
const requestId = `ttreq-${randomUUID()}`;
|
|
1387
|
+
const startedAt = Date.now();
|
|
1388
|
+
const queuedMs = startedAt - receivedAt;
|
|
1389
|
+
this.activeRequests += 1;
|
|
1390
|
+
this.clearIdleTimer();
|
|
1391
|
+
let status = 500;
|
|
1392
|
+
let parsed = null;
|
|
1393
|
+
let failure = null;
|
|
1394
|
+
try {
|
|
1395
|
+
await this.ensureModelReady();
|
|
1396
|
+
const forwarded = await this.forwardGeneration(route, body);
|
|
1397
|
+
status = forwarded.status;
|
|
1398
|
+
parsed = forwarded.parsed;
|
|
1399
|
+
res.writeHead(status, {
|
|
1400
|
+
...copyResponseHeaders(forwarded.headers),
|
|
1401
|
+
"content-length": String(Buffer.byteLength(forwarded.body))
|
|
1402
|
+
});
|
|
1403
|
+
res.end(forwarded.body);
|
|
1404
|
+
this.requestsSinceStart += 1;
|
|
1405
|
+
failure = errorSummary(parsed, null);
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
failure = err instanceof Error ? err.message : String(err);
|
|
1408
|
+
status = 502;
|
|
1409
|
+
sendJson(res, status, { error: { message: failure } });
|
|
1410
|
+
} finally {
|
|
1411
|
+
const finishedAt = Date.now();
|
|
1412
|
+
this.activeRequests -= 1;
|
|
1413
|
+
this.writeLog({
|
|
1414
|
+
timestamp: new Date(finishedAt).toISOString(),
|
|
1415
|
+
request_id: requestId,
|
|
1416
|
+
route,
|
|
1417
|
+
model_target: {
|
|
1418
|
+
name: this.options.modelName,
|
|
1419
|
+
path: this.options.modelPath
|
|
1420
|
+
},
|
|
1421
|
+
request_bytes: body.byteLength,
|
|
1422
|
+
response_status: status,
|
|
1423
|
+
latency_ms: finishedAt - receivedAt,
|
|
1424
|
+
queued_ms: queuedMs,
|
|
1425
|
+
prompt_tokens: usageNumber(parsed, "prompt_tokens"),
|
|
1426
|
+
completion_tokens: usageNumber(parsed, "completion_tokens"),
|
|
1427
|
+
total_tokens: usageNumber(parsed, "total_tokens"),
|
|
1428
|
+
schema_validity: status < 400 ? true : status === 422 ? false : null,
|
|
1429
|
+
gate_field: this.options.gateField,
|
|
1430
|
+
gate_result: this.extractGateResult(parsed),
|
|
1431
|
+
restart_count: this.restartCount,
|
|
1432
|
+
error_summary: failure
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async ensureModelReady() {
|
|
1437
|
+
if (this.child && this.restartAfterRequests > 0 && this.requestsSinceStart >= this.restartAfterRequests) {
|
|
1438
|
+
await this.restartChild();
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (this.child) {
|
|
1442
|
+
const health = await this.checkHealth();
|
|
1443
|
+
if (health.ok) return;
|
|
1444
|
+
await this.restartChild();
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
await this.startChild();
|
|
1448
|
+
}
|
|
1449
|
+
async startChild() {
|
|
1450
|
+
if (this.internalPort == null) {
|
|
1451
|
+
this.internalPort = await allocatePort("127.0.0.1");
|
|
1452
|
+
}
|
|
1453
|
+
const env = {
|
|
1454
|
+
...process.env,
|
|
1455
|
+
...this.options.env,
|
|
1456
|
+
TT_HOST: "127.0.0.1",
|
|
1457
|
+
TT_PORT: String(this.internalPort)
|
|
1458
|
+
};
|
|
1459
|
+
this.child = this.spawnModelServer(this.options.python, this.options.args, {
|
|
1460
|
+
stdio: "inherit",
|
|
1461
|
+
env
|
|
1462
|
+
});
|
|
1463
|
+
this.child.once("exit", () => {
|
|
1464
|
+
if (!this.stoppingChild) {
|
|
1465
|
+
this.child = null;
|
|
1466
|
+
this.requestsSinceStart = 0;
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
try {
|
|
1470
|
+
await this.waitForHealth();
|
|
1471
|
+
this.scheduleIdleStop();
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
await this.stopChild();
|
|
1474
|
+
throw err;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
async restartChild() {
|
|
1478
|
+
await this.stopChild();
|
|
1479
|
+
this.restartCount += 1;
|
|
1480
|
+
this.requestsSinceStart = 0;
|
|
1481
|
+
await this.startChild();
|
|
1482
|
+
}
|
|
1483
|
+
async stopChild() {
|
|
1484
|
+
if (!this.child) return;
|
|
1485
|
+
const child = this.child;
|
|
1486
|
+
this.child = null;
|
|
1487
|
+
this.requestsSinceStart = 0;
|
|
1488
|
+
this.stoppingChild = true;
|
|
1489
|
+
try {
|
|
1490
|
+
child.kill("SIGTERM");
|
|
1491
|
+
await Promise.race([
|
|
1492
|
+
new Promise((resolve4) => child.once("exit", () => resolve4())),
|
|
1493
|
+
sleep(5e3).then(() => {
|
|
1494
|
+
if (child.exitCode == null && child.signalCode == null) child.kill("SIGKILL");
|
|
1495
|
+
})
|
|
1496
|
+
]);
|
|
1497
|
+
} finally {
|
|
1498
|
+
this.stoppingChild = false;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
async checkHealth() {
|
|
1502
|
+
if (this.internalPort == null) return { ok: false, body: null };
|
|
1503
|
+
const controller = new AbortController();
|
|
1504
|
+
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
1505
|
+
try {
|
|
1506
|
+
const response = await fetch(`http://127.0.0.1:${this.internalPort}/health`, {
|
|
1507
|
+
signal: controller.signal
|
|
1508
|
+
});
|
|
1509
|
+
const text = await response.text();
|
|
1510
|
+
return { ok: response.ok, body: tryJsonParse(text) };
|
|
1511
|
+
} catch {
|
|
1512
|
+
return { ok: false, body: null };
|
|
1513
|
+
} finally {
|
|
1514
|
+
clearTimeout(timeout);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
async waitForHealth() {
|
|
1518
|
+
const deadline = Date.now() + 12e4;
|
|
1519
|
+
while (Date.now() < deadline) {
|
|
1520
|
+
const health = await this.checkHealth();
|
|
1521
|
+
if (health.ok) return;
|
|
1522
|
+
if (!this.child) break;
|
|
1523
|
+
await sleep(50);
|
|
1524
|
+
}
|
|
1525
|
+
throw new Error("Model server did not become healthy within 120 seconds");
|
|
1526
|
+
}
|
|
1527
|
+
async forwardGeneration(route, body) {
|
|
1528
|
+
if (this.internalPort == null) throw new Error("Internal server port is not allocated");
|
|
1529
|
+
const response = await fetch(`http://127.0.0.1:${this.internalPort}${route}`, {
|
|
1530
|
+
method: "POST",
|
|
1531
|
+
headers: { "content-type": "application/json" },
|
|
1532
|
+
body
|
|
1533
|
+
});
|
|
1534
|
+
const text = await response.text();
|
|
1535
|
+
return {
|
|
1536
|
+
status: response.status,
|
|
1537
|
+
headers: response.headers,
|
|
1538
|
+
body: text,
|
|
1539
|
+
parsed: tryJsonParse(text)
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
extractGateResult(response) {
|
|
1543
|
+
const content = extractAssistantContent(response);
|
|
1544
|
+
if (!content) return null;
|
|
1545
|
+
const parsedContent = tryJsonParse(content);
|
|
1546
|
+
return readPath(parsedContent, this.options.gateField);
|
|
1547
|
+
}
|
|
1548
|
+
scheduleIdleStop() {
|
|
1549
|
+
this.clearIdleTimer();
|
|
1550
|
+
if (!this.child || this.activeRequests > 0 || this.queuedRequests > 0) return;
|
|
1551
|
+
this.idleTimer = setTimeout(() => {
|
|
1552
|
+
this.stopChild().catch((err) => {
|
|
1553
|
+
this.writeDiagnosticError(err);
|
|
1554
|
+
});
|
|
1555
|
+
}, this.idleTimeoutMs);
|
|
1556
|
+
}
|
|
1557
|
+
clearIdleTimer() {
|
|
1558
|
+
if (!this.idleTimer) return;
|
|
1559
|
+
clearTimeout(this.idleTimer);
|
|
1560
|
+
this.idleTimer = null;
|
|
1561
|
+
}
|
|
1562
|
+
writeLog(record) {
|
|
1563
|
+
if (this.logRecordOverride) {
|
|
1564
|
+
this.logRecordOverride(record);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const line = `${JSON.stringify(record)}
|
|
1568
|
+
`;
|
|
1569
|
+
if (this.options.logFile) {
|
|
1570
|
+
mkdirSync2(dirname(this.options.logFile), { recursive: true });
|
|
1571
|
+
appendFileSync(this.options.logFile, line, "utf8");
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
process.stderr.write(line);
|
|
1575
|
+
}
|
|
1576
|
+
writeDiagnosticError(err) {
|
|
1577
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1578
|
+
process.stderr.write(`${JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), error: message })}
|
|
1579
|
+
`);
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
async function startManagedModelServer(options) {
|
|
1583
|
+
const server = new ManagedModelServer(options);
|
|
1584
|
+
const stop = () => {
|
|
1585
|
+
server.stop().catch(() => void 0);
|
|
1586
|
+
};
|
|
1587
|
+
process.once("SIGINT", stop);
|
|
1588
|
+
process.once("SIGTERM", stop);
|
|
1589
|
+
try {
|
|
1590
|
+
await server.run();
|
|
1591
|
+
} finally {
|
|
1592
|
+
process.off("SIGINT", stop);
|
|
1593
|
+
process.off("SIGTERM", stop);
|
|
1594
|
+
await server.stop();
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1077
1598
|
// src/commands/models.ts
|
|
1078
1599
|
var REFERENCE_SERVER_SCRIPT = String.raw`#!/usr/bin/env python3
|
|
1079
1600
|
import json
|
|
@@ -1585,7 +2106,7 @@ async function downloadUrlToFile(url, outputPath) {
|
|
|
1585
2106
|
if (!res.body) {
|
|
1586
2107
|
throw new Error("Download failed: response body was empty");
|
|
1587
2108
|
}
|
|
1588
|
-
|
|
2109
|
+
mkdirSync3(dirname2(outputPath), { recursive: true });
|
|
1589
2110
|
const file = createWriteStream(outputPath);
|
|
1590
2111
|
const contentLength = parseContentLength(res.headers.get("content-length"));
|
|
1591
2112
|
const progress = createDownloadProgress(
|
|
@@ -1713,7 +2234,7 @@ function directoryHasFiles(path) {
|
|
|
1713
2234
|
function extractArchive(archivePath, targetDir, force) {
|
|
1714
2235
|
if (directoryHasFiles(targetDir) && !force) return targetDir;
|
|
1715
2236
|
if (existsSync4(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
1716
|
-
|
|
2237
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
1717
2238
|
execFileSync("tar", ["-xzf", archivePath, "-C", targetDir], { stdio: "inherit" });
|
|
1718
2239
|
return targetDir;
|
|
1719
2240
|
}
|
|
@@ -1761,7 +2282,7 @@ function loadJsonSchemaForServe(schemaPath) {
|
|
|
1761
2282
|
}
|
|
1762
2283
|
function writeReferenceServerScript(cacheDir) {
|
|
1763
2284
|
const scriptDir = join2(cacheDir, "_server");
|
|
1764
|
-
|
|
2285
|
+
mkdirSync3(scriptDir, { recursive: true });
|
|
1765
2286
|
const scriptPath = join2(scriptDir, "openai_reference_server.py");
|
|
1766
2287
|
writeFileSync3(scriptPath, REFERENCE_SERVER_SCRIPT, "utf8");
|
|
1767
2288
|
return scriptPath;
|
|
@@ -1808,16 +2329,34 @@ function quoteCommandPart(part) {
|
|
|
1808
2329
|
if (/^[a-zA-Z0-9_./:=@-]+$/.test(part)) return part;
|
|
1809
2330
|
return `'${part.replace(/'/g, "'\\''")}'`;
|
|
1810
2331
|
}
|
|
1811
|
-
function
|
|
2332
|
+
function parseServeInteger(value, label) {
|
|
2333
|
+
const parsed = Number(value);
|
|
2334
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
2335
|
+
throw new Error(`${label} must be a non-negative integer.`);
|
|
2336
|
+
}
|
|
2337
|
+
return parsed;
|
|
2338
|
+
}
|
|
2339
|
+
function printServeCommand(command, args, env, managed) {
|
|
1812
2340
|
const envKeys = Object.keys(env).sort();
|
|
1813
2341
|
const commandLine = [command, ...args].map(quoteCommandPart).join(" ");
|
|
1814
2342
|
if (isJsonMode()) {
|
|
1815
|
-
return printJson({ command, args, env_keys: envKeys, command_line: commandLine });
|
|
2343
|
+
return printJson({ command, args, env_keys: envKeys, command_line: commandLine, managed });
|
|
1816
2344
|
}
|
|
1817
|
-
|
|
2345
|
+
const fields = [
|
|
1818
2346
|
["Command", commandLine],
|
|
1819
2347
|
["Environment", envKeys.join(", ")]
|
|
1820
|
-
]
|
|
2348
|
+
];
|
|
2349
|
+
if (managed?.enabled) {
|
|
2350
|
+
fields.push(
|
|
2351
|
+
["Managed", "enabled"],
|
|
2352
|
+
["Wrapper", `http://${managed.host}:${managed.port}`],
|
|
2353
|
+
["Idle timeout", `${managed.idle_timeout_seconds}s`],
|
|
2354
|
+
["Restart after", managed.restart_after_requests === 0 ? "disabled" : `${managed.restart_after_requests} requests`],
|
|
2355
|
+
["Gate field", managed.gate_field],
|
|
2356
|
+
["Log file", managed.log_file]
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
printDetail(fields);
|
|
1821
2360
|
}
|
|
1822
2361
|
function registerModelsCommands(parent) {
|
|
1823
2362
|
const models = parent.command("models").description("Manage fine-tuned models");
|
|
@@ -1921,7 +2460,7 @@ function registerModelsCommands(parent) {
|
|
|
1921
2460
|
rmSync(venvDir, { recursive: true, force: true });
|
|
1922
2461
|
}
|
|
1923
2462
|
if (!existsSync4(venvPython)) {
|
|
1924
|
-
|
|
2463
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
1925
2464
|
execFileSync(commands[0][0], commands[0].slice(1), { stdio: "inherit" });
|
|
1926
2465
|
}
|
|
1927
2466
|
execFileSync(commands[1][0], commands[1].slice(1), { stdio: "inherit" });
|
|
@@ -1938,10 +2477,10 @@ function registerModelsCommands(parent) {
|
|
|
1938
2477
|
printSuccess(`Serving runtime ready at ${venvDir}`);
|
|
1939
2478
|
console.log(`Use it with: tt models serve <model-id> --spec ${DEFAULT_SPEC_FILE}`);
|
|
1940
2479
|
});
|
|
1941
|
-
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("--json-schema <path>", "JSON Schema file to enforce by default for chat completions").option("--json-repair-attempts <n>", "Number of JSON/schema repair retries", "1").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) => {
|
|
2480
|
+
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("--json-schema <path>", "JSON Schema file to enforce by default for chat completions").option("--json-repair-attempts <n>", "Number of JSON/schema repair retries", "1").option("--trust-remote-code", "Pass trust_remote_code=True to transformers").option("--managed", "Run a local lifecycle manager in front of the model server").option("--idle-timeout <seconds>", "Managed mode idle timeout before stopping the model", "300").option("--restart-after-requests <n>", "Managed mode restart threshold; 0 disables request-count restarts", "100").option("--gate-field <field>", "Response JSON field to log as the managed serving gate result", "should_process").option("--log-file <path>", "Write managed serving JSONL request logs to a file").option("--print-command", "Print the underlying Python command without starting it").action(async (target, cmdOpts) => {
|
|
1942
2481
|
const opts = parent.opts();
|
|
1943
2482
|
const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
|
|
1944
|
-
|
|
2483
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
1945
2484
|
const python = resolveServePython(cacheDir, cmdOpts.python);
|
|
1946
2485
|
if (!cmdOpts.printCommand) ensureServingRuntime(python, cacheDir);
|
|
1947
2486
|
const serveTarget = await resolveServeTarget(
|
|
@@ -1976,9 +2515,26 @@ function registerModelsCommands(parent) {
|
|
|
1976
2515
|
if (systemPrompt) env.TT_SYSTEM_PROMPT = systemPrompt;
|
|
1977
2516
|
if (cmdOpts.jsonSchema) env.TT_JSON_SCHEMA = loadJsonSchemaForServe(cmdOpts.jsonSchema);
|
|
1978
2517
|
const args = [serverScript];
|
|
1979
|
-
|
|
2518
|
+
const idleTimeoutSeconds = parseServeInteger(cmdOpts.idleTimeout, "--idle-timeout");
|
|
2519
|
+
const restartAfterRequests = parseServeInteger(
|
|
2520
|
+
cmdOpts.restartAfterRequests,
|
|
2521
|
+
"--restart-after-requests"
|
|
2522
|
+
);
|
|
2523
|
+
const publicPort = parseServeInteger(cmdOpts.port, "--port");
|
|
2524
|
+
const managedConfig = cmdOpts.managed ? {
|
|
2525
|
+
enabled: true,
|
|
2526
|
+
host: String(cmdOpts.host),
|
|
2527
|
+
port: publicPort,
|
|
2528
|
+
idle_timeout_seconds: idleTimeoutSeconds,
|
|
2529
|
+
restart_after_requests: restartAfterRequests,
|
|
2530
|
+
gate_field: String(cmdOpts.gateField),
|
|
2531
|
+
log_file: cmdOpts.logFile ? resolve2(cmdOpts.logFile) : void 0
|
|
2532
|
+
} : void 0;
|
|
2533
|
+
if (cmdOpts.printCommand) return printServeCommand(python, args, env, managedConfig);
|
|
1980
2534
|
if (!isJsonMode()) {
|
|
1981
|
-
printSuccess(
|
|
2535
|
+
printSuccess(
|
|
2536
|
+
cmdOpts.managed ? `Serving ${serveTarget.modelName} through a managed local wrapper` : `Serving ${serveTarget.modelName} from ${serveTarget.modelPath}`
|
|
2537
|
+
);
|
|
1982
2538
|
if (systemPrompt && specPath) {
|
|
1983
2539
|
printSuccess(`Behavior spec prompt loaded from ${specPath}`);
|
|
1984
2540
|
} else if (cmdOpts.specPrompt !== false) {
|
|
@@ -1989,9 +2545,32 @@ function registerModelsCommands(parent) {
|
|
|
1989
2545
|
console.log(`OpenAI-compatible endpoint: http://${cmdOpts.host}:${cmdOpts.port}/v1/chat/completions`);
|
|
1990
2546
|
console.log(`Health check: http://${cmdOpts.host}:${cmdOpts.port}/health`);
|
|
1991
2547
|
console.log(`Python runtime: ${python}`);
|
|
2548
|
+
if (cmdOpts.managed) {
|
|
2549
|
+
console.log(`Managed idle timeout: ${idleTimeoutSeconds}s`);
|
|
2550
|
+
console.log(
|
|
2551
|
+
`Managed restart threshold: ${restartAfterRequests === 0 ? "disabled" : `${restartAfterRequests} requests`}`
|
|
2552
|
+
);
|
|
2553
|
+
console.log(`Managed gate field: ${cmdOpts.gateField}`);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
if (cmdOpts.managed) {
|
|
2557
|
+
await startManagedModelServer({
|
|
2558
|
+
publicHost: String(cmdOpts.host),
|
|
2559
|
+
publicPort,
|
|
2560
|
+
python,
|
|
2561
|
+
args,
|
|
2562
|
+
env,
|
|
2563
|
+
modelName: serveTarget.modelName,
|
|
2564
|
+
modelPath: serveTarget.modelPath,
|
|
2565
|
+
idleTimeoutSeconds,
|
|
2566
|
+
restartAfterRequests,
|
|
2567
|
+
gateField: String(cmdOpts.gateField),
|
|
2568
|
+
logFile: cmdOpts.logFile ? resolve2(cmdOpts.logFile) : void 0
|
|
2569
|
+
});
|
|
2570
|
+
return;
|
|
1992
2571
|
}
|
|
1993
2572
|
await new Promise((resolvePromise, reject) => {
|
|
1994
|
-
const child =
|
|
2573
|
+
const child = spawn2(python, args, {
|
|
1995
2574
|
stdio: "inherit",
|
|
1996
2575
|
env: { ...process.env, ...env }
|
|
1997
2576
|
});
|
|
@@ -2305,7 +2884,7 @@ function registerPushCommand(parent) {
|
|
|
2305
2884
|
|
|
2306
2885
|
// src/index.ts
|
|
2307
2886
|
var program = new Command();
|
|
2308
|
-
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.
|
|
2887
|
+
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.16").option("-k, --api-key <key>", "API key (overrides stored key)").option(
|
|
2309
2888
|
"-u, --base-url <url>",
|
|
2310
2889
|
"API base URL (default: https://tunedtensor.com)"
|
|
2311
2890
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|