@tuned-tensor/cli 0.4.14 → 0.4.15

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 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)) {
@@ -1009,11 +1089,11 @@ function registerDatasetsCommands(parent) {
1009
1089
  }
1010
1090
 
1011
1091
  // src/commands/models.ts
1012
- import { spawn, execFileSync } from "child_process";
1092
+ import { spawn as spawn2, execFileSync } from "child_process";
1013
1093
  import { createHash } from "crypto";
1014
1094
  import {
1015
1095
  existsSync as existsSync4,
1016
- mkdirSync as mkdirSync2,
1096
+ mkdirSync as mkdirSync3,
1017
1097
  statSync as statSync3,
1018
1098
  createWriteStream,
1019
1099
  unlinkSync,
@@ -1023,7 +1103,7 @@ import {
1023
1103
  rmSync
1024
1104
  } from "fs";
1025
1105
  import { homedir as homedir2 } from "os";
1026
- import { dirname, join as join2, resolve as resolve2, basename as basename2 } from "path";
1106
+ import { dirname as dirname2, join as join2, resolve as resolve2, basename as basename2 } from "path";
1027
1107
  import { Readable, Transform } from "stream";
1028
1108
  import { pipeline } from "stream/promises";
1029
1109
 
@@ -1074,6 +1154,444 @@ Run \`tt init\` to create one.`
1074
1154
  return raw;
1075
1155
  }
1076
1156
 
1157
+ // src/serve-manager.ts
1158
+ import { spawn } from "child_process";
1159
+ import { randomUUID } from "crypto";
1160
+ import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
1161
+ import { createServer } from "http";
1162
+ import { createServer as createNetServer } from "net";
1163
+ import { dirname } from "path";
1164
+ var COMPLETION_ROUTES = /* @__PURE__ */ new Set(["/v1/chat/completions", "/chat/completions"]);
1165
+ function parsePositiveInteger(value, fallback) {
1166
+ return Number.isInteger(value) && value >= 0 ? value : fallback;
1167
+ }
1168
+ function sleep(ms) {
1169
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1170
+ }
1171
+ function readRequestBody(req) {
1172
+ return new Promise((resolve4, reject) => {
1173
+ const chunks = [];
1174
+ req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1175
+ req.on("end", () => resolve4(Buffer.concat(chunks)));
1176
+ req.on("error", reject);
1177
+ });
1178
+ }
1179
+ function sendJson(res, status, payload) {
1180
+ const body = Buffer.from(JSON.stringify(payload), "utf8");
1181
+ res.writeHead(status, {
1182
+ "content-type": "application/json",
1183
+ "content-length": String(body.byteLength),
1184
+ "access-control-allow-origin": "*",
1185
+ "access-control-allow-headers": "authorization, content-type",
1186
+ "access-control-allow-methods": "GET, POST, OPTIONS"
1187
+ });
1188
+ res.end(body);
1189
+ }
1190
+ function copyResponseHeaders(headers) {
1191
+ const copied = {
1192
+ "access-control-allow-origin": "*",
1193
+ "access-control-allow-headers": "authorization, content-type",
1194
+ "access-control-allow-methods": "GET, POST, OPTIONS"
1195
+ };
1196
+ const contentType = headers.get("content-type");
1197
+ if (contentType) copied["content-type"] = contentType;
1198
+ return copied;
1199
+ }
1200
+ function tryJsonParse(value) {
1201
+ try {
1202
+ return JSON.parse(value);
1203
+ } catch {
1204
+ return null;
1205
+ }
1206
+ }
1207
+ function readPath(value, path) {
1208
+ if (!path) return null;
1209
+ let current = value;
1210
+ for (const part of path.split(".")) {
1211
+ if (!part) return null;
1212
+ if (!current || typeof current !== "object" || !(part in current)) return null;
1213
+ current = current[part];
1214
+ }
1215
+ return current ?? null;
1216
+ }
1217
+ function extractAssistantContent(response) {
1218
+ if (!response || typeof response !== "object") return null;
1219
+ const choices = response.choices;
1220
+ if (!Array.isArray(choices) || choices.length === 0) return null;
1221
+ const first = choices[0];
1222
+ if (!first || typeof first !== "object") return null;
1223
+ const message = first.message;
1224
+ if (!message || typeof message !== "object") return null;
1225
+ const content = message.content;
1226
+ return typeof content === "string" ? content : null;
1227
+ }
1228
+ function usageNumber(response, key) {
1229
+ if (!response || typeof response !== "object") return null;
1230
+ const usage = response.usage;
1231
+ if (!usage || typeof usage !== "object") return null;
1232
+ const value = usage[key];
1233
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1234
+ }
1235
+ function errorSummary(response, fallback) {
1236
+ if (!response || typeof response !== "object") return fallback;
1237
+ const error = response.error;
1238
+ if (!error || typeof error !== "object") return fallback;
1239
+ const message = error.message;
1240
+ return typeof message === "string" ? message : fallback;
1241
+ }
1242
+ async function allocatePort(host) {
1243
+ return new Promise((resolve4, reject) => {
1244
+ const server = createNetServer();
1245
+ server.on("error", reject);
1246
+ server.listen(0, host, () => {
1247
+ const address = server.address();
1248
+ if (!address || typeof address === "string") {
1249
+ server.close(() => reject(new Error("Could not allocate an internal port")));
1250
+ return;
1251
+ }
1252
+ const port = address.port;
1253
+ server.close(() => resolve4(port));
1254
+ });
1255
+ });
1256
+ }
1257
+ var ManagedModelServer = class {
1258
+ constructor(options) {
1259
+ this.options = options;
1260
+ this.spawnModelServer = options.spawnModelServer ?? spawn;
1261
+ this.idleTimeoutMs = parsePositiveInteger(options.idleTimeoutSeconds, 300) * 1e3;
1262
+ this.restartAfterRequests = parsePositiveInteger(options.restartAfterRequests, 100);
1263
+ this.logRecordOverride = options.logRecord;
1264
+ }
1265
+ spawnModelServer;
1266
+ idleTimeoutMs;
1267
+ restartAfterRequests;
1268
+ logRecordOverride;
1269
+ server = null;
1270
+ child = null;
1271
+ internalPort = null;
1272
+ queue = Promise.resolve();
1273
+ idleTimer = null;
1274
+ activeRequests = 0;
1275
+ queuedRequests = 0;
1276
+ requestsSinceStart = 0;
1277
+ restartCount = 0;
1278
+ stoppingChild = false;
1279
+ get isModelRunning() {
1280
+ return this.child != null;
1281
+ }
1282
+ get restarts() {
1283
+ return this.restartCount;
1284
+ }
1285
+ async start() {
1286
+ if (this.server) return;
1287
+ this.internalPort = await allocatePort("127.0.0.1");
1288
+ this.server = createServer((req, res) => {
1289
+ this.handle(req, res).catch((err) => {
1290
+ sendJson(res, 500, { error: { message: err instanceof Error ? err.message : String(err) } });
1291
+ });
1292
+ });
1293
+ await new Promise((resolve4, reject) => {
1294
+ this.server?.once("error", reject);
1295
+ this.server?.listen(this.options.publicPort, this.options.publicHost, () => {
1296
+ this.server?.off("error", reject);
1297
+ resolve4();
1298
+ });
1299
+ });
1300
+ }
1301
+ async stop() {
1302
+ this.clearIdleTimer();
1303
+ await this.stopChild();
1304
+ if (!this.server) return;
1305
+ const server = this.server;
1306
+ this.server = null;
1307
+ await new Promise((resolve4, reject) => {
1308
+ server.close((err) => err ? reject(err) : resolve4());
1309
+ });
1310
+ }
1311
+ async run() {
1312
+ await this.start();
1313
+ await new Promise((resolve4, reject) => {
1314
+ this.server?.once("error", reject);
1315
+ this.server?.once("close", resolve4);
1316
+ });
1317
+ }
1318
+ async handle(req, res) {
1319
+ const route = new URL(req.url || "/", "http://localhost").pathname;
1320
+ if (req.method === "OPTIONS") {
1321
+ sendJson(res, 204, {});
1322
+ return;
1323
+ }
1324
+ if (req.method === "GET" && route === "/health") {
1325
+ await this.handleHealth(res);
1326
+ return;
1327
+ }
1328
+ if (req.method === "POST" && COMPLETION_ROUTES.has(route)) {
1329
+ const receivedAt = Date.now();
1330
+ const body = await readRequestBody(req);
1331
+ this.queuedRequests += 1;
1332
+ const work = this.queue.then(() => this.processGeneration(route, body, res, receivedAt));
1333
+ this.queue = work.catch(() => void 0);
1334
+ try {
1335
+ await work;
1336
+ } finally {
1337
+ this.queuedRequests -= 1;
1338
+ this.scheduleIdleStop();
1339
+ }
1340
+ return;
1341
+ }
1342
+ sendJson(res, 404, { error: { message: "Not found" } });
1343
+ }
1344
+ async handleHealth(res) {
1345
+ if (!this.child) {
1346
+ sendJson(res, 200, {
1347
+ status: "ok",
1348
+ wrapper: "ok",
1349
+ model_status: "cold",
1350
+ restart_count: this.restartCount
1351
+ });
1352
+ return;
1353
+ }
1354
+ if (this.activeRequests > 0 || this.queuedRequests > 0) {
1355
+ sendJson(res, 200, {
1356
+ status: "ok",
1357
+ wrapper: "ok",
1358
+ model_status: "busy",
1359
+ restart_count: this.restartCount
1360
+ });
1361
+ return;
1362
+ }
1363
+ const health = await this.checkHealth();
1364
+ if (health.ok) {
1365
+ sendJson(res, 200, {
1366
+ status: "ok",
1367
+ wrapper: "ok",
1368
+ model_status: "warm",
1369
+ restart_count: this.restartCount,
1370
+ model: health.body
1371
+ });
1372
+ return;
1373
+ }
1374
+ await this.restartChild();
1375
+ sendJson(res, 200, {
1376
+ status: "ok",
1377
+ wrapper: "ok",
1378
+ model_status: "restarted",
1379
+ restart_count: this.restartCount
1380
+ });
1381
+ }
1382
+ async processGeneration(route, body, res, receivedAt) {
1383
+ const requestId = `ttreq-${randomUUID()}`;
1384
+ const startedAt = Date.now();
1385
+ const queuedMs = startedAt - receivedAt;
1386
+ this.activeRequests += 1;
1387
+ this.clearIdleTimer();
1388
+ let status = 500;
1389
+ let parsed = null;
1390
+ let failure = null;
1391
+ try {
1392
+ await this.ensureModelReady();
1393
+ const forwarded = await this.forwardGeneration(route, body);
1394
+ status = forwarded.status;
1395
+ parsed = forwarded.parsed;
1396
+ res.writeHead(status, {
1397
+ ...copyResponseHeaders(forwarded.headers),
1398
+ "content-length": String(Buffer.byteLength(forwarded.body))
1399
+ });
1400
+ res.end(forwarded.body);
1401
+ this.requestsSinceStart += 1;
1402
+ failure = errorSummary(parsed, null);
1403
+ } catch (err) {
1404
+ failure = err instanceof Error ? err.message : String(err);
1405
+ status = 502;
1406
+ sendJson(res, status, { error: { message: failure } });
1407
+ } finally {
1408
+ const finishedAt = Date.now();
1409
+ this.activeRequests -= 1;
1410
+ this.writeLog({
1411
+ timestamp: new Date(finishedAt).toISOString(),
1412
+ request_id: requestId,
1413
+ route,
1414
+ model_target: {
1415
+ name: this.options.modelName,
1416
+ path: this.options.modelPath
1417
+ },
1418
+ request_bytes: body.byteLength,
1419
+ response_status: status,
1420
+ latency_ms: finishedAt - receivedAt,
1421
+ queued_ms: queuedMs,
1422
+ prompt_tokens: usageNumber(parsed, "prompt_tokens"),
1423
+ completion_tokens: usageNumber(parsed, "completion_tokens"),
1424
+ total_tokens: usageNumber(parsed, "total_tokens"),
1425
+ schema_validity: status < 400 ? true : status === 422 ? false : null,
1426
+ gate_field: this.options.gateField,
1427
+ gate_result: this.extractGateResult(parsed),
1428
+ restart_count: this.restartCount,
1429
+ error_summary: failure
1430
+ });
1431
+ }
1432
+ }
1433
+ async ensureModelReady() {
1434
+ if (this.child && this.restartAfterRequests > 0 && this.requestsSinceStart >= this.restartAfterRequests) {
1435
+ await this.restartChild();
1436
+ return;
1437
+ }
1438
+ if (this.child) {
1439
+ const health = await this.checkHealth();
1440
+ if (health.ok) return;
1441
+ await this.restartChild();
1442
+ return;
1443
+ }
1444
+ await this.startChild();
1445
+ }
1446
+ async startChild() {
1447
+ if (this.internalPort == null) {
1448
+ this.internalPort = await allocatePort("127.0.0.1");
1449
+ }
1450
+ const env = {
1451
+ ...process.env,
1452
+ ...this.options.env,
1453
+ TT_HOST: "127.0.0.1",
1454
+ TT_PORT: String(this.internalPort)
1455
+ };
1456
+ this.child = this.spawnModelServer(this.options.python, this.options.args, {
1457
+ stdio: "inherit",
1458
+ env
1459
+ });
1460
+ this.child.once("exit", () => {
1461
+ if (!this.stoppingChild) {
1462
+ this.child = null;
1463
+ this.requestsSinceStart = 0;
1464
+ }
1465
+ });
1466
+ try {
1467
+ await this.waitForHealth();
1468
+ this.scheduleIdleStop();
1469
+ } catch (err) {
1470
+ await this.stopChild();
1471
+ throw err;
1472
+ }
1473
+ }
1474
+ async restartChild() {
1475
+ await this.stopChild();
1476
+ this.restartCount += 1;
1477
+ this.requestsSinceStart = 0;
1478
+ await this.startChild();
1479
+ }
1480
+ async stopChild() {
1481
+ if (!this.child) return;
1482
+ const child = this.child;
1483
+ this.child = null;
1484
+ this.requestsSinceStart = 0;
1485
+ this.stoppingChild = true;
1486
+ try {
1487
+ child.kill("SIGTERM");
1488
+ await Promise.race([
1489
+ new Promise((resolve4) => child.once("exit", () => resolve4())),
1490
+ sleep(5e3).then(() => {
1491
+ if (child.exitCode == null && child.signalCode == null) child.kill("SIGKILL");
1492
+ })
1493
+ ]);
1494
+ } finally {
1495
+ this.stoppingChild = false;
1496
+ }
1497
+ }
1498
+ async checkHealth() {
1499
+ if (this.internalPort == null) return { ok: false, body: null };
1500
+ const controller = new AbortController();
1501
+ const timeout = setTimeout(() => controller.abort(), 2e3);
1502
+ try {
1503
+ const response = await fetch(`http://127.0.0.1:${this.internalPort}/health`, {
1504
+ signal: controller.signal
1505
+ });
1506
+ const text = await response.text();
1507
+ return { ok: response.ok, body: tryJsonParse(text) };
1508
+ } catch {
1509
+ return { ok: false, body: null };
1510
+ } finally {
1511
+ clearTimeout(timeout);
1512
+ }
1513
+ }
1514
+ async waitForHealth() {
1515
+ const deadline = Date.now() + 12e4;
1516
+ while (Date.now() < deadline) {
1517
+ const health = await this.checkHealth();
1518
+ if (health.ok) return;
1519
+ if (!this.child) break;
1520
+ await sleep(50);
1521
+ }
1522
+ throw new Error("Model server did not become healthy within 120 seconds");
1523
+ }
1524
+ async forwardGeneration(route, body) {
1525
+ if (this.internalPort == null) throw new Error("Internal server port is not allocated");
1526
+ const response = await fetch(`http://127.0.0.1:${this.internalPort}${route}`, {
1527
+ method: "POST",
1528
+ headers: { "content-type": "application/json" },
1529
+ body
1530
+ });
1531
+ const text = await response.text();
1532
+ return {
1533
+ status: response.status,
1534
+ headers: response.headers,
1535
+ body: text,
1536
+ parsed: tryJsonParse(text)
1537
+ };
1538
+ }
1539
+ extractGateResult(response) {
1540
+ const content = extractAssistantContent(response);
1541
+ if (!content) return null;
1542
+ const parsedContent = tryJsonParse(content);
1543
+ return readPath(parsedContent, this.options.gateField);
1544
+ }
1545
+ scheduleIdleStop() {
1546
+ this.clearIdleTimer();
1547
+ if (!this.child || this.activeRequests > 0 || this.queuedRequests > 0) return;
1548
+ this.idleTimer = setTimeout(() => {
1549
+ this.stopChild().catch((err) => {
1550
+ this.writeDiagnosticError(err);
1551
+ });
1552
+ }, this.idleTimeoutMs);
1553
+ }
1554
+ clearIdleTimer() {
1555
+ if (!this.idleTimer) return;
1556
+ clearTimeout(this.idleTimer);
1557
+ this.idleTimer = null;
1558
+ }
1559
+ writeLog(record) {
1560
+ if (this.logRecordOverride) {
1561
+ this.logRecordOverride(record);
1562
+ return;
1563
+ }
1564
+ const line = `${JSON.stringify(record)}
1565
+ `;
1566
+ if (this.options.logFile) {
1567
+ mkdirSync2(dirname(this.options.logFile), { recursive: true });
1568
+ appendFileSync(this.options.logFile, line, "utf8");
1569
+ return;
1570
+ }
1571
+ process.stderr.write(line);
1572
+ }
1573
+ writeDiagnosticError(err) {
1574
+ const message = err instanceof Error ? err.message : String(err);
1575
+ process.stderr.write(`${JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), error: message })}
1576
+ `);
1577
+ }
1578
+ };
1579
+ async function startManagedModelServer(options) {
1580
+ const server = new ManagedModelServer(options);
1581
+ const stop = () => {
1582
+ server.stop().catch(() => void 0);
1583
+ };
1584
+ process.once("SIGINT", stop);
1585
+ process.once("SIGTERM", stop);
1586
+ try {
1587
+ await server.run();
1588
+ } finally {
1589
+ process.off("SIGINT", stop);
1590
+ process.off("SIGTERM", stop);
1591
+ await server.stop();
1592
+ }
1593
+ }
1594
+
1077
1595
  // src/commands/models.ts
1078
1596
  var REFERENCE_SERVER_SCRIPT = String.raw`#!/usr/bin/env python3
1079
1597
  import json
@@ -1585,7 +2103,7 @@ async function downloadUrlToFile(url, outputPath) {
1585
2103
  if (!res.body) {
1586
2104
  throw new Error("Download failed: response body was empty");
1587
2105
  }
1588
- mkdirSync2(dirname(outputPath), { recursive: true });
2106
+ mkdirSync3(dirname2(outputPath), { recursive: true });
1589
2107
  const file = createWriteStream(outputPath);
1590
2108
  const contentLength = parseContentLength(res.headers.get("content-length"));
1591
2109
  const progress = createDownloadProgress(
@@ -1713,7 +2231,7 @@ function directoryHasFiles(path) {
1713
2231
  function extractArchive(archivePath, targetDir, force) {
1714
2232
  if (directoryHasFiles(targetDir) && !force) return targetDir;
1715
2233
  if (existsSync4(targetDir)) rmSync(targetDir, { recursive: true, force: true });
1716
- mkdirSync2(targetDir, { recursive: true });
2234
+ mkdirSync3(targetDir, { recursive: true });
1717
2235
  execFileSync("tar", ["-xzf", archivePath, "-C", targetDir], { stdio: "inherit" });
1718
2236
  return targetDir;
1719
2237
  }
@@ -1761,7 +2279,7 @@ function loadJsonSchemaForServe(schemaPath) {
1761
2279
  }
1762
2280
  function writeReferenceServerScript(cacheDir) {
1763
2281
  const scriptDir = join2(cacheDir, "_server");
1764
- mkdirSync2(scriptDir, { recursive: true });
2282
+ mkdirSync3(scriptDir, { recursive: true });
1765
2283
  const scriptPath = join2(scriptDir, "openai_reference_server.py");
1766
2284
  writeFileSync3(scriptPath, REFERENCE_SERVER_SCRIPT, "utf8");
1767
2285
  return scriptPath;
@@ -1808,16 +2326,34 @@ function quoteCommandPart(part) {
1808
2326
  if (/^[a-zA-Z0-9_./:=@-]+$/.test(part)) return part;
1809
2327
  return `'${part.replace(/'/g, "'\\''")}'`;
1810
2328
  }
1811
- function printServeCommand(command, args, env) {
2329
+ function parseServeInteger(value, label) {
2330
+ const parsed = Number(value);
2331
+ if (!Number.isInteger(parsed) || parsed < 0) {
2332
+ throw new Error(`${label} must be a non-negative integer.`);
2333
+ }
2334
+ return parsed;
2335
+ }
2336
+ function printServeCommand(command, args, env, managed) {
1812
2337
  const envKeys = Object.keys(env).sort();
1813
2338
  const commandLine = [command, ...args].map(quoteCommandPart).join(" ");
1814
2339
  if (isJsonMode()) {
1815
- return printJson({ command, args, env_keys: envKeys, command_line: commandLine });
2340
+ return printJson({ command, args, env_keys: envKeys, command_line: commandLine, managed });
1816
2341
  }
1817
- printDetail([
2342
+ const fields = [
1818
2343
  ["Command", commandLine],
1819
2344
  ["Environment", envKeys.join(", ")]
1820
- ]);
2345
+ ];
2346
+ if (managed?.enabled) {
2347
+ fields.push(
2348
+ ["Managed", "enabled"],
2349
+ ["Wrapper", `http://${managed.host}:${managed.port}`],
2350
+ ["Idle timeout", `${managed.idle_timeout_seconds}s`],
2351
+ ["Restart after", managed.restart_after_requests === 0 ? "disabled" : `${managed.restart_after_requests} requests`],
2352
+ ["Gate field", managed.gate_field],
2353
+ ["Log file", managed.log_file]
2354
+ );
2355
+ }
2356
+ printDetail(fields);
1821
2357
  }
1822
2358
  function registerModelsCommands(parent) {
1823
2359
  const models = parent.command("models").description("Manage fine-tuned models");
@@ -1921,7 +2457,7 @@ function registerModelsCommands(parent) {
1921
2457
  rmSync(venvDir, { recursive: true, force: true });
1922
2458
  }
1923
2459
  if (!existsSync4(venvPython)) {
1924
- mkdirSync2(cacheDir, { recursive: true });
2460
+ mkdirSync3(cacheDir, { recursive: true });
1925
2461
  execFileSync(commands[0][0], commands[0].slice(1), { stdio: "inherit" });
1926
2462
  }
1927
2463
  execFileSync(commands[1][0], commands[1].slice(1), { stdio: "inherit" });
@@ -1938,10 +2474,10 @@ function registerModelsCommands(parent) {
1938
2474
  printSuccess(`Serving runtime ready at ${venvDir}`);
1939
2475
  console.log(`Use it with: tt models serve <model-id> --spec ${DEFAULT_SPEC_FILE}`);
1940
2476
  });
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) => {
2477
+ 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
2478
  const opts = parent.opts();
1943
2479
  const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
1944
- mkdirSync2(cacheDir, { recursive: true });
2480
+ mkdirSync3(cacheDir, { recursive: true });
1945
2481
  const python = resolveServePython(cacheDir, cmdOpts.python);
1946
2482
  if (!cmdOpts.printCommand) ensureServingRuntime(python, cacheDir);
1947
2483
  const serveTarget = await resolveServeTarget(
@@ -1976,9 +2512,26 @@ function registerModelsCommands(parent) {
1976
2512
  if (systemPrompt) env.TT_SYSTEM_PROMPT = systemPrompt;
1977
2513
  if (cmdOpts.jsonSchema) env.TT_JSON_SCHEMA = loadJsonSchemaForServe(cmdOpts.jsonSchema);
1978
2514
  const args = [serverScript];
1979
- if (cmdOpts.printCommand) return printServeCommand(python, args, env);
2515
+ const idleTimeoutSeconds = parseServeInteger(cmdOpts.idleTimeout, "--idle-timeout");
2516
+ const restartAfterRequests = parseServeInteger(
2517
+ cmdOpts.restartAfterRequests,
2518
+ "--restart-after-requests"
2519
+ );
2520
+ const publicPort = parseServeInteger(cmdOpts.port, "--port");
2521
+ const managedConfig = cmdOpts.managed ? {
2522
+ enabled: true,
2523
+ host: String(cmdOpts.host),
2524
+ port: publicPort,
2525
+ idle_timeout_seconds: idleTimeoutSeconds,
2526
+ restart_after_requests: restartAfterRequests,
2527
+ gate_field: String(cmdOpts.gateField),
2528
+ log_file: cmdOpts.logFile ? resolve2(cmdOpts.logFile) : void 0
2529
+ } : void 0;
2530
+ if (cmdOpts.printCommand) return printServeCommand(python, args, env, managedConfig);
1980
2531
  if (!isJsonMode()) {
1981
- printSuccess(`Serving ${serveTarget.modelName} from ${serveTarget.modelPath}`);
2532
+ printSuccess(
2533
+ cmdOpts.managed ? `Serving ${serveTarget.modelName} through a managed local wrapper` : `Serving ${serveTarget.modelName} from ${serveTarget.modelPath}`
2534
+ );
1982
2535
  if (systemPrompt && specPath) {
1983
2536
  printSuccess(`Behavior spec prompt loaded from ${specPath}`);
1984
2537
  } else if (cmdOpts.specPrompt !== false) {
@@ -1989,9 +2542,32 @@ function registerModelsCommands(parent) {
1989
2542
  console.log(`OpenAI-compatible endpoint: http://${cmdOpts.host}:${cmdOpts.port}/v1/chat/completions`);
1990
2543
  console.log(`Health check: http://${cmdOpts.host}:${cmdOpts.port}/health`);
1991
2544
  console.log(`Python runtime: ${python}`);
2545
+ if (cmdOpts.managed) {
2546
+ console.log(`Managed idle timeout: ${idleTimeoutSeconds}s`);
2547
+ console.log(
2548
+ `Managed restart threshold: ${restartAfterRequests === 0 ? "disabled" : `${restartAfterRequests} requests`}`
2549
+ );
2550
+ console.log(`Managed gate field: ${cmdOpts.gateField}`);
2551
+ }
2552
+ }
2553
+ if (cmdOpts.managed) {
2554
+ await startManagedModelServer({
2555
+ publicHost: String(cmdOpts.host),
2556
+ publicPort,
2557
+ python,
2558
+ args,
2559
+ env,
2560
+ modelName: serveTarget.modelName,
2561
+ modelPath: serveTarget.modelPath,
2562
+ idleTimeoutSeconds,
2563
+ restartAfterRequests,
2564
+ gateField: String(cmdOpts.gateField),
2565
+ logFile: cmdOpts.logFile ? resolve2(cmdOpts.logFile) : void 0
2566
+ });
2567
+ return;
1992
2568
  }
1993
2569
  await new Promise((resolvePromise, reject) => {
1994
- const child = spawn(python, args, {
2570
+ const child = spawn2(python, args, {
1995
2571
  stdio: "inherit",
1996
2572
  env: { ...process.env, ...env }
1997
2573
  });
@@ -2305,7 +2881,7 @@ function registerPushCommand(parent) {
2305
2881
 
2306
2882
  // src/index.ts
2307
2883
  var program = new Command();
2308
- program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.14").option("-k, --api-key <key>", "API key (overrides stored key)").option(
2884
+ program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.15").option("-k, --api-key <key>", "API key (overrides stored key)").option(
2309
2885
  "-u, --base-url <url>",
2310
2886
  "API base URL (default: https://tunedtensor.com)"
2311
2887
  ).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {