@tuned-tensor/cli 0.4.13 → 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/README.md +17 -139
- package/dist/index.js +808 -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)) {
|
|
@@ -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
|
|
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
|
|
@@ -1091,6 +1609,10 @@ DEFAULT_MAX_TOKENS = int(os.environ.get("TT_MAX_TOKENS", "512"))
|
|
|
1091
1609
|
DEFAULT_TEMPERATURE = float(os.environ.get("TT_TEMPERATURE", "0.7"))
|
|
1092
1610
|
TRUST_REMOTE_CODE = os.environ.get("TT_TRUST_REMOTE_CODE", "false").lower() in {"1", "true", "yes", "y"}
|
|
1093
1611
|
REQUESTED_DEVICE = os.environ.get("TT_DEVICE", "auto").lower()
|
|
1612
|
+
JSON_REPAIR_ATTEMPTS = int(os.environ.get("TT_JSON_REPAIR_ATTEMPTS", "1"))
|
|
1613
|
+
DEFAULT_JSON_SCHEMA = None
|
|
1614
|
+
if os.environ.get("TT_JSON_SCHEMA"):
|
|
1615
|
+
DEFAULT_JSON_SCHEMA = json.loads(os.environ["TT_JSON_SCHEMA"])
|
|
1094
1616
|
|
|
1095
1617
|
|
|
1096
1618
|
def choose_device(torch):
|
|
@@ -1179,6 +1701,50 @@ def normalize_messages(raw_messages):
|
|
|
1179
1701
|
return messages
|
|
1180
1702
|
|
|
1181
1703
|
|
|
1704
|
+
def response_json_contract(body):
|
|
1705
|
+
response_format = body.get("response_format")
|
|
1706
|
+
schema = DEFAULT_JSON_SCHEMA
|
|
1707
|
+
mode = "json_schema" if schema else "text"
|
|
1708
|
+
|
|
1709
|
+
if isinstance(response_format, dict):
|
|
1710
|
+
format_type = response_format.get("type")
|
|
1711
|
+
if format_type == "json_object":
|
|
1712
|
+
mode = "json_object"
|
|
1713
|
+
elif format_type == "json_schema":
|
|
1714
|
+
mode = "json_schema"
|
|
1715
|
+
json_schema = response_format.get("json_schema")
|
|
1716
|
+
if isinstance(json_schema, dict):
|
|
1717
|
+
schema = json_schema.get("schema") if isinstance(json_schema.get("schema"), dict) else json_schema
|
|
1718
|
+
|
|
1719
|
+
return mode, schema
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
def with_json_contract(messages, mode, schema):
|
|
1723
|
+
if mode == "text":
|
|
1724
|
+
return messages
|
|
1725
|
+
|
|
1726
|
+
if schema:
|
|
1727
|
+
contract = (
|
|
1728
|
+
"Return only a valid JSON object matching this JSON Schema. "
|
|
1729
|
+
"Do not include markdown, code fences, comments, or prose outside the JSON object.\n"
|
|
1730
|
+
+ json.dumps(schema, ensure_ascii=False, separators=(",", ":"))
|
|
1731
|
+
)
|
|
1732
|
+
else:
|
|
1733
|
+
contract = (
|
|
1734
|
+
"Return only a valid JSON object. Do not include markdown, code fences, "
|
|
1735
|
+
"comments, or prose outside the JSON object."
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
contracted = [dict(message) for message in messages]
|
|
1739
|
+
for message in contracted:
|
|
1740
|
+
if message["role"] == "system":
|
|
1741
|
+
message["content"] = (message["content"].rstrip() + "\n\n" + contract).strip()
|
|
1742
|
+
return contracted
|
|
1743
|
+
|
|
1744
|
+
contracted.insert(0, {"role": "system", "content": contract})
|
|
1745
|
+
return contracted
|
|
1746
|
+
|
|
1747
|
+
|
|
1182
1748
|
def render_prompt(messages):
|
|
1183
1749
|
chat_template = getattr(tokenizer, "chat_template", None)
|
|
1184
1750
|
if hasattr(tokenizer, "apply_chat_template") and chat_template:
|
|
@@ -1219,6 +1785,142 @@ def generate_completion(messages, max_tokens, temperature):
|
|
|
1219
1785
|
return content, prompt_tokens, int(completion_ids.shape[-1])
|
|
1220
1786
|
|
|
1221
1787
|
|
|
1788
|
+
def extract_json_value(content):
|
|
1789
|
+
decoder = json.JSONDecoder()
|
|
1790
|
+
text = content.strip()
|
|
1791
|
+
try:
|
|
1792
|
+
return json.loads(text), None
|
|
1793
|
+
except Exception:
|
|
1794
|
+
pass
|
|
1795
|
+
|
|
1796
|
+
for index, char in enumerate(text):
|
|
1797
|
+
if char not in "{[":
|
|
1798
|
+
continue
|
|
1799
|
+
try:
|
|
1800
|
+
value, end = decoder.raw_decode(text[index:])
|
|
1801
|
+
trailing = text[index + end :].strip()
|
|
1802
|
+
if trailing:
|
|
1803
|
+
continue
|
|
1804
|
+
return value, None
|
|
1805
|
+
except Exception:
|
|
1806
|
+
continue
|
|
1807
|
+
return None, "response is not valid JSON"
|
|
1808
|
+
|
|
1809
|
+
|
|
1810
|
+
def json_type_matches(value, expected):
|
|
1811
|
+
if expected == "object":
|
|
1812
|
+
return isinstance(value, dict)
|
|
1813
|
+
if expected == "array":
|
|
1814
|
+
return isinstance(value, list)
|
|
1815
|
+
if expected == "string":
|
|
1816
|
+
return isinstance(value, str)
|
|
1817
|
+
if expected == "boolean":
|
|
1818
|
+
return isinstance(value, bool)
|
|
1819
|
+
if expected == "integer":
|
|
1820
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
1821
|
+
if expected == "number":
|
|
1822
|
+
return (isinstance(value, int) or isinstance(value, float)) and not isinstance(value, bool)
|
|
1823
|
+
if expected == "null":
|
|
1824
|
+
return value is None
|
|
1825
|
+
return True
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
def validate_json_schema(value, schema, path="$"):
|
|
1829
|
+
if not isinstance(schema, dict):
|
|
1830
|
+
return []
|
|
1831
|
+
|
|
1832
|
+
errors = []
|
|
1833
|
+
expected_type = schema.get("type")
|
|
1834
|
+
if isinstance(expected_type, list):
|
|
1835
|
+
if not any(json_type_matches(value, item) for item in expected_type):
|
|
1836
|
+
errors.append(f"{path} has wrong type")
|
|
1837
|
+
return errors
|
|
1838
|
+
elif isinstance(expected_type, str) and not json_type_matches(value, expected_type):
|
|
1839
|
+
errors.append(f"{path} must be {expected_type}")
|
|
1840
|
+
return errors
|
|
1841
|
+
|
|
1842
|
+
if "enum" in schema and value not in schema["enum"]:
|
|
1843
|
+
errors.append(f"{path} must be one of {schema['enum']}")
|
|
1844
|
+
|
|
1845
|
+
if isinstance(value, dict):
|
|
1846
|
+
required = schema.get("required") if isinstance(schema.get("required"), list) else []
|
|
1847
|
+
for key in required:
|
|
1848
|
+
if key not in value:
|
|
1849
|
+
errors.append(f"{path}.{key} is required")
|
|
1850
|
+
|
|
1851
|
+
properties = schema.get("properties") if isinstance(schema.get("properties"), dict) else {}
|
|
1852
|
+
for key, child_schema in properties.items():
|
|
1853
|
+
if key in value:
|
|
1854
|
+
errors.extend(validate_json_schema(value[key], child_schema, f"{path}.{key}"))
|
|
1855
|
+
|
|
1856
|
+
if schema.get("additionalProperties") is False:
|
|
1857
|
+
extras = sorted(set(value.keys()) - set(properties.keys()))
|
|
1858
|
+
for key in extras:
|
|
1859
|
+
errors.append(f"{path}.{key} is not allowed")
|
|
1860
|
+
|
|
1861
|
+
if isinstance(value, list) and isinstance(schema.get("items"), dict):
|
|
1862
|
+
for index, item in enumerate(value):
|
|
1863
|
+
errors.extend(validate_json_schema(item, schema["items"], f"{path}[{index}]"))
|
|
1864
|
+
|
|
1865
|
+
return errors
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
def normalize_json_content(content, mode, schema):
|
|
1869
|
+
if mode == "text":
|
|
1870
|
+
return content, []
|
|
1871
|
+
|
|
1872
|
+
value, error = extract_json_value(content)
|
|
1873
|
+
if error:
|
|
1874
|
+
return content, [error]
|
|
1875
|
+
if mode in {"json_object", "json_schema"} and not isinstance(value, dict):
|
|
1876
|
+
return content, ["response must be a JSON object"]
|
|
1877
|
+
|
|
1878
|
+
errors = validate_json_schema(value, schema) if schema else []
|
|
1879
|
+
if errors:
|
|
1880
|
+
return content, errors
|
|
1881
|
+
|
|
1882
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":")), []
|
|
1883
|
+
|
|
1884
|
+
|
|
1885
|
+
def generate_validated_completion(messages, max_tokens, temperature, mode, schema):
|
|
1886
|
+
contracted_messages = with_json_contract(messages, mode, schema)
|
|
1887
|
+
attempts = max(0, JSON_REPAIR_ATTEMPTS) + 1
|
|
1888
|
+
last_content = ""
|
|
1889
|
+
last_prompt_tokens = 0
|
|
1890
|
+
last_completion_tokens = 0
|
|
1891
|
+
last_errors = []
|
|
1892
|
+
|
|
1893
|
+
for attempt in range(attempts):
|
|
1894
|
+
content, prompt_tokens, completion_tokens = generate_completion(
|
|
1895
|
+
contracted_messages,
|
|
1896
|
+
max_tokens,
|
|
1897
|
+
temperature,
|
|
1898
|
+
)
|
|
1899
|
+
normalized, errors = normalize_json_content(content, mode, schema)
|
|
1900
|
+
if not errors:
|
|
1901
|
+
return normalized, prompt_tokens, completion_tokens, []
|
|
1902
|
+
|
|
1903
|
+
last_content = content
|
|
1904
|
+
last_prompt_tokens = prompt_tokens
|
|
1905
|
+
last_completion_tokens = completion_tokens
|
|
1906
|
+
last_errors = errors
|
|
1907
|
+
|
|
1908
|
+
if attempt < attempts - 1:
|
|
1909
|
+
contracted_messages = contracted_messages + [
|
|
1910
|
+
{"role": "assistant", "content": content},
|
|
1911
|
+
{
|
|
1912
|
+
"role": "user",
|
|
1913
|
+
"content": (
|
|
1914
|
+
"The previous response was invalid: "
|
|
1915
|
+
+ "; ".join(errors[:5])
|
|
1916
|
+
+ ". Return only a corrected JSON object."
|
|
1917
|
+
),
|
|
1918
|
+
},
|
|
1919
|
+
]
|
|
1920
|
+
|
|
1921
|
+
return last_content, last_prompt_tokens, last_completion_tokens, last_errors
|
|
1922
|
+
|
|
1923
|
+
|
|
1222
1924
|
class Handler(BaseHTTPRequestHandler):
|
|
1223
1925
|
server_version = "TunedTensorReferenceServer/0.1"
|
|
1224
1926
|
|
|
@@ -1267,7 +1969,23 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1267
1969
|
max_tokens = int(body.get("max_tokens") or DEFAULT_MAX_TOKENS)
|
|
1268
1970
|
temperature = float(body.get("temperature") if body.get("temperature") is not None else DEFAULT_TEMPERATURE)
|
|
1269
1971
|
messages = normalize_messages(raw_messages)
|
|
1270
|
-
|
|
1972
|
+
json_mode, json_schema = response_json_contract(body)
|
|
1973
|
+
content, prompt_tokens, completion_tokens, validation_errors = generate_validated_completion(
|
|
1974
|
+
messages,
|
|
1975
|
+
max_tokens,
|
|
1976
|
+
temperature,
|
|
1977
|
+
json_mode,
|
|
1978
|
+
json_schema,
|
|
1979
|
+
)
|
|
1980
|
+
if validation_errors:
|
|
1981
|
+
self.send_json(422, {
|
|
1982
|
+
"error": {
|
|
1983
|
+
"message": "Model did not produce valid JSON for the requested response_format.",
|
|
1984
|
+
"details": validation_errors,
|
|
1985
|
+
"content": content,
|
|
1986
|
+
}
|
|
1987
|
+
})
|
|
1988
|
+
return
|
|
1271
1989
|
|
|
1272
1990
|
self.send_json(200, {
|
|
1273
1991
|
"id": "chatcmpl-" + uuid.uuid4().hex,
|
|
@@ -1385,7 +2103,7 @@ async function downloadUrlToFile(url, outputPath) {
|
|
|
1385
2103
|
if (!res.body) {
|
|
1386
2104
|
throw new Error("Download failed: response body was empty");
|
|
1387
2105
|
}
|
|
1388
|
-
|
|
2106
|
+
mkdirSync3(dirname2(outputPath), { recursive: true });
|
|
1389
2107
|
const file = createWriteStream(outputPath);
|
|
1390
2108
|
const contentLength = parseContentLength(res.headers.get("content-length"));
|
|
1391
2109
|
const progress = createDownloadProgress(
|
|
@@ -1513,7 +2231,7 @@ function directoryHasFiles(path) {
|
|
|
1513
2231
|
function extractArchive(archivePath, targetDir, force) {
|
|
1514
2232
|
if (directoryHasFiles(targetDir) && !force) return targetDir;
|
|
1515
2233
|
if (existsSync4(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
1516
|
-
|
|
2234
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
1517
2235
|
execFileSync("tar", ["-xzf", archivePath, "-C", targetDir], { stdio: "inherit" });
|
|
1518
2236
|
return targetDir;
|
|
1519
2237
|
}
|
|
@@ -1547,9 +2265,21 @@ function loadSystemPromptFromSpec(specPath) {
|
|
|
1547
2265
|
const spec = JSON.parse(readFileSync6(specPath, "utf8"));
|
|
1548
2266
|
return buildSystemPrompt(spec);
|
|
1549
2267
|
}
|
|
2268
|
+
function loadJsonSchemaForServe(schemaPath) {
|
|
2269
|
+
const resolved = resolve2(schemaPath);
|
|
2270
|
+
if (!existsSync4(resolved) || !statSync3(resolved).isFile()) {
|
|
2271
|
+
throw new Error(`JSON schema file not found: ${schemaPath}`);
|
|
2272
|
+
}
|
|
2273
|
+
try {
|
|
2274
|
+
const schema = JSON.parse(readFileSync6(resolved, "utf8"));
|
|
2275
|
+
return JSON.stringify(schema);
|
|
2276
|
+
} catch (err) {
|
|
2277
|
+
throw new Error(`JSON schema file is not valid JSON: ${schemaPath}`);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
1550
2280
|
function writeReferenceServerScript(cacheDir) {
|
|
1551
2281
|
const scriptDir = join2(cacheDir, "_server");
|
|
1552
|
-
|
|
2282
|
+
mkdirSync3(scriptDir, { recursive: true });
|
|
1553
2283
|
const scriptPath = join2(scriptDir, "openai_reference_server.py");
|
|
1554
2284
|
writeFileSync3(scriptPath, REFERENCE_SERVER_SCRIPT, "utf8");
|
|
1555
2285
|
return scriptPath;
|
|
@@ -1596,16 +2326,34 @@ function quoteCommandPart(part) {
|
|
|
1596
2326
|
if (/^[a-zA-Z0-9_./:=@-]+$/.test(part)) return part;
|
|
1597
2327
|
return `'${part.replace(/'/g, "'\\''")}'`;
|
|
1598
2328
|
}
|
|
1599
|
-
function
|
|
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) {
|
|
1600
2337
|
const envKeys = Object.keys(env).sort();
|
|
1601
2338
|
const commandLine = [command, ...args].map(quoteCommandPart).join(" ");
|
|
1602
2339
|
if (isJsonMode()) {
|
|
1603
|
-
return printJson({ command, args, env_keys: envKeys, command_line: commandLine });
|
|
2340
|
+
return printJson({ command, args, env_keys: envKeys, command_line: commandLine, managed });
|
|
1604
2341
|
}
|
|
1605
|
-
|
|
2342
|
+
const fields = [
|
|
1606
2343
|
["Command", commandLine],
|
|
1607
2344
|
["Environment", envKeys.join(", ")]
|
|
1608
|
-
]
|
|
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);
|
|
1609
2357
|
}
|
|
1610
2358
|
function registerModelsCommands(parent) {
|
|
1611
2359
|
const models = parent.command("models").description("Manage fine-tuned models");
|
|
@@ -1709,7 +2457,7 @@ function registerModelsCommands(parent) {
|
|
|
1709
2457
|
rmSync(venvDir, { recursive: true, force: true });
|
|
1710
2458
|
}
|
|
1711
2459
|
if (!existsSync4(venvPython)) {
|
|
1712
|
-
|
|
2460
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
1713
2461
|
execFileSync(commands[0][0], commands[0].slice(1), { stdio: "inherit" });
|
|
1714
2462
|
}
|
|
1715
2463
|
execFileSync(commands[1][0], commands[1].slice(1), { stdio: "inherit" });
|
|
@@ -1726,10 +2474,10 @@ function registerModelsCommands(parent) {
|
|
|
1726
2474
|
printSuccess(`Serving runtime ready at ${venvDir}`);
|
|
1727
2475
|
console.log(`Use it with: tt models serve <model-id> --spec ${DEFAULT_SPEC_FILE}`);
|
|
1728
2476
|
});
|
|
1729
|
-
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) => {
|
|
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) => {
|
|
1730
2478
|
const opts = parent.opts();
|
|
1731
2479
|
const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
|
|
1732
|
-
|
|
2480
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
1733
2481
|
const python = resolveServePython(cacheDir, cmdOpts.python);
|
|
1734
2482
|
if (!cmdOpts.printCommand) ensureServingRuntime(python, cacheDir);
|
|
1735
2483
|
const serveTarget = await resolveServeTarget(
|
|
@@ -1757,14 +2505,33 @@ function registerModelsCommands(parent) {
|
|
|
1757
2505
|
TT_PORT: String(cmdOpts.port),
|
|
1758
2506
|
TT_MAX_TOKENS: String(cmdOpts.maxTokens),
|
|
1759
2507
|
TT_TEMPERATURE: String(cmdOpts.temperature),
|
|
2508
|
+
TT_JSON_REPAIR_ATTEMPTS: String(cmdOpts.jsonRepairAttempts),
|
|
1760
2509
|
TT_TRUST_REMOTE_CODE: cmdOpts.trustRemoteCode ? "true" : "false",
|
|
1761
2510
|
TT_DEVICE: String(cmdOpts.device)
|
|
1762
2511
|
};
|
|
1763
2512
|
if (systemPrompt) env.TT_SYSTEM_PROMPT = systemPrompt;
|
|
2513
|
+
if (cmdOpts.jsonSchema) env.TT_JSON_SCHEMA = loadJsonSchemaForServe(cmdOpts.jsonSchema);
|
|
1764
2514
|
const args = [serverScript];
|
|
1765
|
-
|
|
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);
|
|
1766
2531
|
if (!isJsonMode()) {
|
|
1767
|
-
printSuccess(
|
|
2532
|
+
printSuccess(
|
|
2533
|
+
cmdOpts.managed ? `Serving ${serveTarget.modelName} through a managed local wrapper` : `Serving ${serveTarget.modelName} from ${serveTarget.modelPath}`
|
|
2534
|
+
);
|
|
1768
2535
|
if (systemPrompt && specPath) {
|
|
1769
2536
|
printSuccess(`Behavior spec prompt loaded from ${specPath}`);
|
|
1770
2537
|
} else if (cmdOpts.specPrompt !== false) {
|
|
@@ -1775,9 +2542,32 @@ function registerModelsCommands(parent) {
|
|
|
1775
2542
|
console.log(`OpenAI-compatible endpoint: http://${cmdOpts.host}:${cmdOpts.port}/v1/chat/completions`);
|
|
1776
2543
|
console.log(`Health check: http://${cmdOpts.host}:${cmdOpts.port}/health`);
|
|
1777
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;
|
|
1778
2568
|
}
|
|
1779
2569
|
await new Promise((resolvePromise, reject) => {
|
|
1780
|
-
const child =
|
|
2570
|
+
const child = spawn2(python, args, {
|
|
1781
2571
|
stdio: "inherit",
|
|
1782
2572
|
env: { ...process.env, ...env }
|
|
1783
2573
|
});
|
|
@@ -2091,7 +2881,7 @@ function registerPushCommand(parent) {
|
|
|
2091
2881
|
|
|
2092
2882
|
// src/index.ts
|
|
2093
2883
|
var program = new Command();
|
|
2094
|
-
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.
|
|
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(
|
|
2095
2885
|
"-u, --base-url <url>",
|
|
2096
2886
|
"API base URL (default: https://tunedtensor.com)"
|
|
2097
2887
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|