@tuned-tensor/cli 0.4.16 → 0.4.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/dist/index.js +414 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,9 +55,25 @@ tt models base
|
|
|
55
55
|
tt balance
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
Export a model to GGUF and package it for Ollama (so it's pluggable like any
|
|
59
|
+
other local model, e.g. in OpenClaw via Ollama's native `/api/chat`):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Convert + quantize to GGUF, write a Modelfile, and run `ollama create`
|
|
63
|
+
tt models export <model-id> --format gguf --quant q4_k_m --ollama
|
|
64
|
+
|
|
65
|
+
# Inspect the planned llama.cpp / ollama commands without running them
|
|
66
|
+
tt models export <model-id> --quant q8_0 --ollama --print-command
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This wraps llama.cpp's `convert_hf_to_gguf.py` + `llama-quantize` and Ollama's
|
|
70
|
+
`ollama create`. Point `tt` at your llama.cpp checkout with `--llama-cpp <dir>`
|
|
71
|
+
(or `--convert-script` / `--quantize-bin`); with `--ollama` the behaviour spec's
|
|
72
|
+
system prompt is embedded as the Modelfile `SYSTEM` block.
|
|
73
|
+
|
|
74
|
+
For the full command reference, including dataset-backed runs, long-example
|
|
75
|
+
policies, eval token budgets, continued fine-tuning, evaluation caps, local model serving,
|
|
76
|
+
configuration, and billing, see the [CLI docs](https://tunedtensor.com/docs/cli).
|
|
61
77
|
|
|
62
78
|
## Development
|
|
63
79
|
|
package/dist/index.js
CHANGED
|
@@ -443,7 +443,8 @@ var SPEC_BODY_KEYS = /* @__PURE__ */ new Set([
|
|
|
443
443
|
"system_prompt",
|
|
444
444
|
"guidelines",
|
|
445
445
|
"constraints",
|
|
446
|
-
"examples"
|
|
446
|
+
"examples",
|
|
447
|
+
"eval_cases"
|
|
447
448
|
]);
|
|
448
449
|
var RUN_INPUT_KEYS = ["run_id", "behavior_spec_id", "spec_snapshot", "run_number"];
|
|
449
450
|
var SpecBodyError = class extends Error {
|
|
@@ -583,6 +584,15 @@ function registerSpecsCommands(parent) {
|
|
|
583
584
|
|
|
584
585
|
// src/commands/runs.ts
|
|
585
586
|
import ora from "ora";
|
|
587
|
+
var LONG_EXAMPLE_POLICIES = ["error", "truncate", "skip"];
|
|
588
|
+
function parseLongExamplesPolicy(value) {
|
|
589
|
+
if (LONG_EXAMPLE_POLICIES.includes(value)) {
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
throw new Error(
|
|
593
|
+
`--long-examples must be one of: ${LONG_EXAMPLE_POLICIES.join(", ")}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
586
596
|
function formatProgress(run) {
|
|
587
597
|
if (run.progress_pct == null && !run.stage_label) return void 0;
|
|
588
598
|
const label = run.stage_label ?? run.current_stage ?? "Progress";
|
|
@@ -791,7 +801,7 @@ Eval Results (${data._evals.length}):`);
|
|
|
791
801
|
);
|
|
792
802
|
}
|
|
793
803
|
});
|
|
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) => {
|
|
804
|
+
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").option("--long-examples <policy>", "Long training row policy: error, truncate, or skip").option("--max-seq-length <tokens>", "Maximum training sequence length in tokens").option("--max-output-tokens <tokens>", "Desired evaluation output budget in tokens").option("--eval-reserved-output-tokens <tokens>", "Minimum evaluation output tokens reserved per row").action(async (specId, cmdOpts) => {
|
|
795
805
|
const opts = parent.opts();
|
|
796
806
|
const body = {};
|
|
797
807
|
if (cmdOpts.augment === false) body.augment = false;
|
|
@@ -819,6 +829,12 @@ Eval Results (${data._evals.length}):`);
|
|
|
819
829
|
if (cmdOpts.loraAlpha) hp.lora_alpha = Number(cmdOpts.loraAlpha);
|
|
820
830
|
if (cmdOpts.maxEvalExamples) hp.max_eval_examples = Number(cmdOpts.maxEvalExamples);
|
|
821
831
|
if (cmdOpts.maxTestEvalExamples) hp.max_test_eval_examples = Number(cmdOpts.maxTestEvalExamples);
|
|
832
|
+
if (cmdOpts.longExamples) hp.long_examples = parseLongExamplesPolicy(cmdOpts.longExamples);
|
|
833
|
+
if (cmdOpts.maxSeqLength) hp.max_seq_length = Number(cmdOpts.maxSeqLength);
|
|
834
|
+
if (cmdOpts.maxOutputTokens) hp.max_output_tokens = Number(cmdOpts.maxOutputTokens);
|
|
835
|
+
if (cmdOpts.evalReservedOutputTokens) {
|
|
836
|
+
hp.eval_reserved_output_tokens = Number(cmdOpts.evalReservedOutputTokens);
|
|
837
|
+
}
|
|
822
838
|
if (cmdOpts.llmJudge === false) body.use_llm_judge = false;
|
|
823
839
|
if (Object.keys(hp).length) body.hyperparameters = hp;
|
|
824
840
|
const fullSpecId = await resolveSpecId(specId, opts);
|
|
@@ -2358,6 +2374,174 @@ function printServeCommand(command, args, env, managed) {
|
|
|
2358
2374
|
}
|
|
2359
2375
|
printDetail(fields);
|
|
2360
2376
|
}
|
|
2377
|
+
var CONVERT_NATIVE_OUTTYPES = /* @__PURE__ */ new Set([
|
|
2378
|
+
"f32",
|
|
2379
|
+
"f16",
|
|
2380
|
+
"bf16",
|
|
2381
|
+
"q8_0",
|
|
2382
|
+
"tq1_0",
|
|
2383
|
+
"tq2_0"
|
|
2384
|
+
]);
|
|
2385
|
+
var QUANTIZE_TYPES = /* @__PURE__ */ new Set([
|
|
2386
|
+
"q4_0",
|
|
2387
|
+
"q4_1",
|
|
2388
|
+
"q5_0",
|
|
2389
|
+
"q5_1",
|
|
2390
|
+
"q2_k",
|
|
2391
|
+
"q2_k_s",
|
|
2392
|
+
"q3_k_s",
|
|
2393
|
+
"q3_k_m",
|
|
2394
|
+
"q3_k_l",
|
|
2395
|
+
"q4_k_s",
|
|
2396
|
+
"q4_k_m",
|
|
2397
|
+
"q5_k_s",
|
|
2398
|
+
"q5_k_m",
|
|
2399
|
+
"q6_k",
|
|
2400
|
+
"iq1_s",
|
|
2401
|
+
"iq1_m",
|
|
2402
|
+
"iq2_xxs",
|
|
2403
|
+
"iq2_xs",
|
|
2404
|
+
"iq2_s",
|
|
2405
|
+
"iq2_m",
|
|
2406
|
+
"iq3_xxs",
|
|
2407
|
+
"iq3_s",
|
|
2408
|
+
"iq3_m",
|
|
2409
|
+
"iq4_nl",
|
|
2410
|
+
"iq4_xs"
|
|
2411
|
+
]);
|
|
2412
|
+
function planQuant(quant) {
|
|
2413
|
+
const lower = quant.trim().toLowerCase();
|
|
2414
|
+
if (!lower) {
|
|
2415
|
+
throw new Error("--quant requires a value (e.g. q4_k_m, q8_0, f16).");
|
|
2416
|
+
}
|
|
2417
|
+
if (CONVERT_NATIVE_OUTTYPES.has(lower)) {
|
|
2418
|
+
return { quant: lower, requiresQuantize: false, convertOuttype: lower };
|
|
2419
|
+
}
|
|
2420
|
+
if (QUANTIZE_TYPES.has(lower)) {
|
|
2421
|
+
return {
|
|
2422
|
+
quant: lower,
|
|
2423
|
+
requiresQuantize: true,
|
|
2424
|
+
convertOuttype: "f16",
|
|
2425
|
+
quantizeType: lower.toUpperCase()
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
const supported = [...CONVERT_NATIVE_OUTTYPES, ...QUANTIZE_TYPES].sort().join(", ");
|
|
2429
|
+
throw new Error(`Unsupported --quant "${quant}". Supported types: ${supported}.`);
|
|
2430
|
+
}
|
|
2431
|
+
function ollamaSlug(name) {
|
|
2432
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
2433
|
+
return slug || "model";
|
|
2434
|
+
}
|
|
2435
|
+
function firstExisting(candidates) {
|
|
2436
|
+
for (const candidate of candidates) {
|
|
2437
|
+
if (candidate && existsSync4(candidate) && statSync3(candidate).isFile()) {
|
|
2438
|
+
return resolve2(candidate);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
function llamaCppDir(explicit) {
|
|
2444
|
+
return explicit || process.env.LLAMA_CPP_DIR || process.env.LLAMA_CPP_HOME || void 0;
|
|
2445
|
+
}
|
|
2446
|
+
function resolveConvertScript(opts, required) {
|
|
2447
|
+
if (opts.convertScript) {
|
|
2448
|
+
const resolved = resolve2(opts.convertScript);
|
|
2449
|
+
if (required && !existsSync4(resolved)) {
|
|
2450
|
+
throw new Error(`Conversion script not found: ${opts.convertScript}`);
|
|
2451
|
+
}
|
|
2452
|
+
return resolved;
|
|
2453
|
+
}
|
|
2454
|
+
const dir = llamaCppDir(opts.llamaCpp);
|
|
2455
|
+
const candidates = dir ? [join2(dir, "convert_hf_to_gguf.py"), join2(dir, "convert-hf-to-gguf.py")] : [];
|
|
2456
|
+
const found = firstExisting(candidates);
|
|
2457
|
+
if (found) return found;
|
|
2458
|
+
if (required) {
|
|
2459
|
+
throw new Error(
|
|
2460
|
+
"Could not find convert_hf_to_gguf.py. Pass --llama-cpp <dir> (a llama.cpp checkout) or --convert-script <path>. Get it from https://github.com/ggml-org/llama.cpp."
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
return dir ? join2(dir, "convert_hf_to_gguf.py") : "convert_hf_to_gguf.py";
|
|
2464
|
+
}
|
|
2465
|
+
function resolveQuantizeBin(opts, required) {
|
|
2466
|
+
if (opts.quantizeBin) {
|
|
2467
|
+
const resolved = resolve2(opts.quantizeBin);
|
|
2468
|
+
if (required && !existsSync4(resolved)) {
|
|
2469
|
+
throw new Error(`llama-quantize binary not found: ${opts.quantizeBin}`);
|
|
2470
|
+
}
|
|
2471
|
+
return resolved;
|
|
2472
|
+
}
|
|
2473
|
+
const dir = llamaCppDir(opts.llamaCpp);
|
|
2474
|
+
if (dir) {
|
|
2475
|
+
const candidates = [
|
|
2476
|
+
join2(dir, "build", "bin", "llama-quantize"),
|
|
2477
|
+
join2(dir, "build", "bin", "quantize"),
|
|
2478
|
+
join2(dir, "llama-quantize"),
|
|
2479
|
+
join2(dir, "quantize")
|
|
2480
|
+
];
|
|
2481
|
+
const found = firstExisting(candidates);
|
|
2482
|
+
if (found) return found;
|
|
2483
|
+
}
|
|
2484
|
+
if (required && !commandExists("llama-quantize")) {
|
|
2485
|
+
throw new Error(
|
|
2486
|
+
"Could not find the llama-quantize binary. Build llama.cpp and pass --llama-cpp <dir> or --quantize-bin <path>, or add llama-quantize to your PATH."
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
return "llama-quantize";
|
|
2490
|
+
}
|
|
2491
|
+
function buildModelfile(ggufBasename, systemPrompt) {
|
|
2492
|
+
const lines = [`FROM ./${ggufBasename}`];
|
|
2493
|
+
if (systemPrompt.trim()) {
|
|
2494
|
+
const escaped = systemPrompt.trim().replace(/"""/g, '\\"\\"\\"');
|
|
2495
|
+
lines.push("", `SYSTEM """${escaped}"""`);
|
|
2496
|
+
}
|
|
2497
|
+
return lines.join("\n") + "\n";
|
|
2498
|
+
}
|
|
2499
|
+
function buildExportStep(name, command, args) {
|
|
2500
|
+
return {
|
|
2501
|
+
name,
|
|
2502
|
+
command,
|
|
2503
|
+
args,
|
|
2504
|
+
command_line: [command, ...args].map(quoteCommandPart).join(" ")
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
function printExportPlan(plan) {
|
|
2508
|
+
if (isJsonMode()) return printJson(plan);
|
|
2509
|
+
printDetail([
|
|
2510
|
+
["Model", `${plan.model.name} (${plan.model.source})`],
|
|
2511
|
+
["Format", plan.format],
|
|
2512
|
+
["Quant", plan.quant],
|
|
2513
|
+
["Output dir", plan.output_dir],
|
|
2514
|
+
["GGUF", plan.gguf_path]
|
|
2515
|
+
]);
|
|
2516
|
+
console.log("\nPlanned steps:");
|
|
2517
|
+
for (const step of plan.steps) {
|
|
2518
|
+
console.log(` ${step.name}: ${step.command_line}`);
|
|
2519
|
+
}
|
|
2520
|
+
if (plan.ollama) {
|
|
2521
|
+
console.log(`
|
|
2522
|
+
Modelfile (${plan.ollama.modelfile_path}):`);
|
|
2523
|
+
console.log(plan.ollama.modelfile.replace(/^/gm, " "));
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function printOpenClawHints(ollamaName) {
|
|
2527
|
+
console.log("\nUse it with OpenClaw via Ollama's native /api/chat (not /v1):");
|
|
2528
|
+
console.log(
|
|
2529
|
+
[
|
|
2530
|
+
" {",
|
|
2531
|
+
" models: { providers: { ollama: {",
|
|
2532
|
+
' api: "ollama",',
|
|
2533
|
+
' baseUrl: "http://127.0.0.1:11434",',
|
|
2534
|
+
` models: [{ id: "${ollamaName}" }]`,
|
|
2535
|
+
" }}}",
|
|
2536
|
+
" }"
|
|
2537
|
+
].join("\n")
|
|
2538
|
+
);
|
|
2539
|
+
console.log("\nThen run it through the infer surface:");
|
|
2540
|
+
console.log(
|
|
2541
|
+
` openclaw infer model run --local --model ollama/${ollamaName} \\
|
|
2542
|
+
--prompt "<payload>" --json`
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2361
2545
|
function registerModelsCommands(parent) {
|
|
2362
2546
|
const models = parent.command("models").description("Manage fine-tuned models");
|
|
2363
2547
|
models.command("base").description("List supported base models").action(async () => {
|
|
@@ -2432,6 +2616,149 @@ function registerModelsCommands(parent) {
|
|
|
2432
2616
|
const size = bytes == null ? "" : ` (${bytes} bytes)`;
|
|
2433
2617
|
printSuccess(`Model downloaded to ${outputPath}${size}`);
|
|
2434
2618
|
});
|
|
2619
|
+
models.command("export").description("Export a fine-tuned model to GGUF and (optionally) package it for Ollama").argument("<target>", "Model ID/prefix, model directory, or .tar.gz artifact").option("--format <format>", "Export format", "gguf").option("--quant <type>", "Quantization type (e.g. q4_k_m, q8_0, f16)", "q4_k_m").option("-o, --output <dir>", "Output directory for the GGUF (and Modelfile)").option("--ollama", "Also write an Ollama Modelfile and run `ollama create`").option("--ollama-name <name>", "Ollama model name (default: tt-<slug>)").option("--no-ollama-create", "With --ollama, write the Modelfile but skip `ollama create`").option("--spec <path>", "Behavior spec JSON to embed as the Ollama SYSTEM prompt").option("--no-spec-prompt", "Do not embed a behavior spec system prompt in the Modelfile").option("--llama-cpp <dir>", "Path to a llama.cpp checkout/build (convert script + quantize binary)").option("--convert-script <path>", "Path to convert_hf_to_gguf.py").option("--quantize-bin <path>", "Path to the llama-quantize binary").option("--ollama-bin <path>", "Path to the ollama binary", "ollama").option("--python <path>", "Python executable to run the conversion script", "python3").option("--cache-dir <path>", "Cache directory for downloaded/extracted models").option("--force-download", "Re-download and re-extract model artifacts").option("-f, --force", "Overwrite existing GGUF/Modelfile outputs").option("--keep-intermediate", "Keep the intermediate f16 GGUF after quantization").option("--print-command", "Print the planned commands without executing them").action(async (target, cmdOpts) => {
|
|
2620
|
+
const opts = parent.opts();
|
|
2621
|
+
const format = String(cmdOpts.format).toLowerCase();
|
|
2622
|
+
if (format !== "gguf") {
|
|
2623
|
+
throw new Error(`Unsupported --format "${cmdOpts.format}". Only "gguf" is supported.`);
|
|
2624
|
+
}
|
|
2625
|
+
const quantPlan = planQuant(String(cmdOpts.quant));
|
|
2626
|
+
const printOnly = Boolean(cmdOpts.printCommand);
|
|
2627
|
+
const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
|
|
2628
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
2629
|
+
const serveTarget = await resolveServeTarget(
|
|
2630
|
+
target,
|
|
2631
|
+
opts,
|
|
2632
|
+
cacheDir,
|
|
2633
|
+
Boolean(cmdOpts.forceDownload)
|
|
2634
|
+
);
|
|
2635
|
+
const slug = ollamaSlug(serveTarget.modelName);
|
|
2636
|
+
const outputDir = resolve2(cmdOpts.output || join2(process.cwd(), `${slug}-gguf`));
|
|
2637
|
+
const finalPath = join2(outputDir, `${slug}.${quantPlan.quant}.gguf`);
|
|
2638
|
+
const intermediatePath = quantPlan.requiresQuantize ? join2(outputDir, `${slug}.f16.gguf`) : void 0;
|
|
2639
|
+
const convertScript = resolveConvertScript(cmdOpts, !printOnly);
|
|
2640
|
+
const convertOutfile = intermediatePath ?? finalPath;
|
|
2641
|
+
const steps = [
|
|
2642
|
+
buildExportStep("convert", cmdOpts.python, [
|
|
2643
|
+
convertScript,
|
|
2644
|
+
serveTarget.modelPath,
|
|
2645
|
+
"--outfile",
|
|
2646
|
+
convertOutfile,
|
|
2647
|
+
"--outtype",
|
|
2648
|
+
quantPlan.convertOuttype
|
|
2649
|
+
])
|
|
2650
|
+
];
|
|
2651
|
+
let quantizeBin;
|
|
2652
|
+
if (quantPlan.requiresQuantize) {
|
|
2653
|
+
quantizeBin = resolveQuantizeBin(cmdOpts, !printOnly);
|
|
2654
|
+
steps.push(
|
|
2655
|
+
buildExportStep("quantize", quantizeBin, [
|
|
2656
|
+
intermediatePath,
|
|
2657
|
+
finalPath,
|
|
2658
|
+
quantPlan.quantizeType
|
|
2659
|
+
])
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
const wantOllama = Boolean(cmdOpts.ollama);
|
|
2663
|
+
const ollamaName = cmdOpts.ollamaName || `tt-${slug}`;
|
|
2664
|
+
let ollamaInfo;
|
|
2665
|
+
let modelfileContent = "";
|
|
2666
|
+
let modelfilePath = "";
|
|
2667
|
+
if (wantOllama) {
|
|
2668
|
+
let systemPrompt = "";
|
|
2669
|
+
if (cmdOpts.specPrompt !== false) {
|
|
2670
|
+
const specPath = findSpecPath(cmdOpts.spec, serveTarget.modelPath);
|
|
2671
|
+
if (cmdOpts.spec && !specPath) {
|
|
2672
|
+
throw new Error(`Spec file not found: ${cmdOpts.spec}`);
|
|
2673
|
+
}
|
|
2674
|
+
if (specPath) systemPrompt = loadSystemPromptFromSpec(specPath);
|
|
2675
|
+
}
|
|
2676
|
+
modelfilePath = join2(outputDir, "Modelfile");
|
|
2677
|
+
modelfileContent = buildModelfile(basename2(finalPath), systemPrompt);
|
|
2678
|
+
const createModel = cmdOpts.ollamaCreate !== false;
|
|
2679
|
+
if (createModel) {
|
|
2680
|
+
steps.push(
|
|
2681
|
+
buildExportStep("ollama-create", cmdOpts.ollamaBin, [
|
|
2682
|
+
"create",
|
|
2683
|
+
ollamaName,
|
|
2684
|
+
"-f",
|
|
2685
|
+
modelfilePath
|
|
2686
|
+
])
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
ollamaInfo = {
|
|
2690
|
+
name: ollamaName,
|
|
2691
|
+
modelfile_path: modelfilePath,
|
|
2692
|
+
modelfile: modelfileContent,
|
|
2693
|
+
create: createModel
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
const plan = {
|
|
2697
|
+
format: "gguf",
|
|
2698
|
+
quant: quantPlan.quant,
|
|
2699
|
+
model: {
|
|
2700
|
+
name: serveTarget.modelName,
|
|
2701
|
+
path: serveTarget.modelPath,
|
|
2702
|
+
source: serveTarget.source,
|
|
2703
|
+
id: serveTarget.modelId
|
|
2704
|
+
},
|
|
2705
|
+
output_dir: outputDir,
|
|
2706
|
+
gguf_path: finalPath,
|
|
2707
|
+
intermediate_path: intermediatePath,
|
|
2708
|
+
steps,
|
|
2709
|
+
ollama: ollamaInfo
|
|
2710
|
+
};
|
|
2711
|
+
if (printOnly) return printExportPlan(plan);
|
|
2712
|
+
if (existsSync4(finalPath) && !cmdOpts.force) {
|
|
2713
|
+
throw new Error(`Output already exists: ${finalPath}. Use --force to overwrite.`);
|
|
2714
|
+
}
|
|
2715
|
+
mkdirSync3(outputDir, { recursive: true });
|
|
2716
|
+
const runStep = (command, args) => execFileSync(command, args, { stdio: "inherit" });
|
|
2717
|
+
if (!isJsonMode()) printSuccess(`Converting ${serveTarget.modelName} \u2192 ${quantPlan.convertOuttype} GGUF`);
|
|
2718
|
+
runStep(cmdOpts.python, [
|
|
2719
|
+
convertScript,
|
|
2720
|
+
serveTarget.modelPath,
|
|
2721
|
+
"--outfile",
|
|
2722
|
+
convertOutfile,
|
|
2723
|
+
"--outtype",
|
|
2724
|
+
quantPlan.convertOuttype
|
|
2725
|
+
]);
|
|
2726
|
+
if (quantPlan.requiresQuantize && quantizeBin) {
|
|
2727
|
+
if (!isJsonMode()) printSuccess(`Quantizing \u2192 ${quantPlan.quantizeType}`);
|
|
2728
|
+
runStep(quantizeBin, [
|
|
2729
|
+
intermediatePath,
|
|
2730
|
+
finalPath,
|
|
2731
|
+
quantPlan.quantizeType
|
|
2732
|
+
]);
|
|
2733
|
+
if (!cmdOpts.keepIntermediate && intermediatePath && existsSync4(intermediatePath)) {
|
|
2734
|
+
rmSync(intermediatePath, { force: true });
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
if (ollamaInfo) {
|
|
2738
|
+
writeFileSync3(modelfilePath, modelfileContent, "utf8");
|
|
2739
|
+
if (!isJsonMode()) printSuccess(`Wrote Modelfile to ${modelfilePath}`);
|
|
2740
|
+
if (ollamaInfo.create) {
|
|
2741
|
+
if (!isJsonMode()) printSuccess(`Creating Ollama model ${ollamaName}`);
|
|
2742
|
+
runStep(cmdOpts.ollamaBin, ["create", ollamaName, "-f", modelfilePath]);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
if (isJsonMode()) {
|
|
2746
|
+
return printJson({
|
|
2747
|
+
format: "gguf",
|
|
2748
|
+
quant: quantPlan.quant,
|
|
2749
|
+
output_dir: outputDir,
|
|
2750
|
+
gguf_path: finalPath,
|
|
2751
|
+
ollama: ollamaInfo ? { name: ollamaName, modelfile_path: modelfilePath, created: ollamaInfo.create } : void 0
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
printSuccess(`GGUF written to ${finalPath}`);
|
|
2755
|
+
if (ollamaInfo?.create) {
|
|
2756
|
+
console.log(`Run it: ollama run ${ollamaName}`);
|
|
2757
|
+
printOpenClawHints(ollamaName);
|
|
2758
|
+
} else if (ollamaInfo) {
|
|
2759
|
+
console.log(`Create the Ollama model: ollama create ${ollamaName} -f ${modelfilePath}`);
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2435
2762
|
models.command("setup-runtime").description("Install an isolated Python runtime for local model serving").option("--python <path>", "Python 3.10-3.12 executable to create the runtime").option("--cache-dir <path>", "Cache directory for the managed runtime").option("-f, --force", "Recreate the runtime if it already exists").option("--print-command", "Print the setup commands without running them").action(async (cmdOpts) => {
|
|
2436
2763
|
const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
|
|
2437
2764
|
const venvDir = runtimeDir(cacheDir);
|
|
@@ -2784,11 +3111,90 @@ function validateSpec(spec) {
|
|
|
2784
3111
|
message: violations.length === 0 ? void 0 : violations.join("; ")
|
|
2785
3112
|
});
|
|
2786
3113
|
}
|
|
3114
|
+
const evalCaseErrors = validateEvalCases(spec);
|
|
3115
|
+
checks.push({
|
|
3116
|
+
name: "Eval cases valid",
|
|
3117
|
+
passed: evalCaseErrors.length === 0,
|
|
3118
|
+
message: evalCaseErrors.length === 0 ? spec.eval_cases?.length ? `${spec.eval_cases.length} executable eval case(s)` : void 0 : evalCaseErrors.join("; ")
|
|
3119
|
+
});
|
|
2787
3120
|
return {
|
|
2788
3121
|
valid: checks.every((c) => c.passed),
|
|
2789
3122
|
checks
|
|
2790
3123
|
};
|
|
2791
3124
|
}
|
|
3125
|
+
function validateEvalCases(spec) {
|
|
3126
|
+
const errors = [];
|
|
3127
|
+
const cases = spec.eval_cases ?? [];
|
|
3128
|
+
for (const [caseIndex, evalCase] of cases.entries()) {
|
|
3129
|
+
const label = `eval_cases[${caseIndex}]`;
|
|
3130
|
+
if (!evalCase || typeof evalCase !== "object") {
|
|
3131
|
+
errors.push(`${label} must be an object`);
|
|
3132
|
+
continue;
|
|
3133
|
+
}
|
|
3134
|
+
if (typeof evalCase.input !== "string" || evalCase.input.trim().length === 0) {
|
|
3135
|
+
errors.push(`${label}.input must be a non-empty string`);
|
|
3136
|
+
}
|
|
3137
|
+
if (evalCase.runtime !== "python") {
|
|
3138
|
+
errors.push(`${label}.runtime must be "python"`);
|
|
3139
|
+
}
|
|
3140
|
+
if (!Array.isArray(evalCase.tests) || evalCase.tests.length === 0) {
|
|
3141
|
+
errors.push(`${label}.tests must contain at least one test`);
|
|
3142
|
+
continue;
|
|
3143
|
+
}
|
|
3144
|
+
for (const [testIndex, test] of evalCase.tests.entries()) {
|
|
3145
|
+
const testLabel = `${label}.tests[${testIndex}]`;
|
|
3146
|
+
if (!test || typeof test !== "object") {
|
|
3147
|
+
errors.push(`${testLabel} must be an object`);
|
|
3148
|
+
continue;
|
|
3149
|
+
}
|
|
3150
|
+
if (test.name !== void 0 && (typeof test.name !== "string" || test.name.trim().length === 0)) {
|
|
3151
|
+
errors.push(`${testLabel}.name must be a non-empty string when provided`);
|
|
3152
|
+
}
|
|
3153
|
+
if (test.args !== void 0 && (!Array.isArray(test.args) || !test.args.every((arg) => typeof arg === "string"))) {
|
|
3154
|
+
errors.push(`${testLabel}.args must be an array of strings`);
|
|
3155
|
+
}
|
|
3156
|
+
if (test.stdin !== void 0 && typeof test.stdin !== "string") {
|
|
3157
|
+
errors.push(`${testLabel}.stdin must be a string`);
|
|
3158
|
+
}
|
|
3159
|
+
if (test.expected_exit_code !== void 0 && (typeof test.expected_exit_code !== "number" || !Number.isInteger(test.expected_exit_code) || test.expected_exit_code < 0 || test.expected_exit_code > 255)) {
|
|
3160
|
+
errors.push(`${testLabel}.expected_exit_code must be an integer between 0 and 255`);
|
|
3161
|
+
}
|
|
3162
|
+
if (test.expected_stdout !== void 0 && typeof test.expected_stdout !== "string") {
|
|
3163
|
+
errors.push(`${testLabel}.expected_stdout must be a string`);
|
|
3164
|
+
}
|
|
3165
|
+
validateFiles(test.files, `${testLabel}.files`, errors);
|
|
3166
|
+
validateFiles(test.expected_files, `${testLabel}.expected_files`, errors);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
return errors;
|
|
3170
|
+
}
|
|
3171
|
+
function validateFiles(files, label, errors) {
|
|
3172
|
+
if (files === void 0) return;
|
|
3173
|
+
if (!Array.isArray(files)) {
|
|
3174
|
+
errors.push(`${label} must be an array`);
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
for (const [index, file] of files.entries()) {
|
|
3178
|
+
const fileLabel = `${label}[${index}]`;
|
|
3179
|
+
if (!file || typeof file !== "object") {
|
|
3180
|
+
errors.push(`${fileLabel} must be an object`);
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
const row = file;
|
|
3184
|
+
if (typeof row.path !== "string" || !isSafeRelativePath(row.path)) {
|
|
3185
|
+
errors.push(`${fileLabel}.path must be a safe relative path`);
|
|
3186
|
+
}
|
|
3187
|
+
if (row.content !== void 0 && typeof row.content !== "string") {
|
|
3188
|
+
errors.push(`${fileLabel}.content must be a string`);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
function isSafeRelativePath(value) {
|
|
3193
|
+
if (value.length === 0 || value.length > 240) return false;
|
|
3194
|
+
if (value.includes("\0") || value.startsWith("/")) return false;
|
|
3195
|
+
const normalized = value.replace(/\\/g, "/");
|
|
3196
|
+
return !normalized.split("/").some((part) => part === "" || part === "." || part === "..");
|
|
3197
|
+
}
|
|
2792
3198
|
function checkExamplesAgainstConstraints(spec) {
|
|
2793
3199
|
const violations = [];
|
|
2794
3200
|
for (const [i, ex] of spec.examples.entries()) {
|
|
@@ -2862,7 +3268,11 @@ function registerPushCommand(parent) {
|
|
|
2862
3268
|
const opts = parent.opts();
|
|
2863
3269
|
const filePath = resolve3(cmdOpts.file);
|
|
2864
3270
|
const spec = loadSpec(cmdOpts.file);
|
|
2865
|
-
const
|
|
3271
|
+
const evalCaseErrors = validateEvalCases(spec);
|
|
3272
|
+
if (evalCaseErrors.length > 0) {
|
|
3273
|
+
throw new Error(`Invalid eval_cases: ${evalCaseErrors.join("; ")}`);
|
|
3274
|
+
}
|
|
3275
|
+
const { id, ...rawBody } = spec;
|
|
2866
3276
|
const body = canonicalizeSpecBaseModel(rawBody);
|
|
2867
3277
|
let data;
|
|
2868
3278
|
if (id) {
|
|
@@ -2884,7 +3294,7 @@ function registerPushCommand(parent) {
|
|
|
2884
3294
|
|
|
2885
3295
|
// src/index.ts
|
|
2886
3296
|
var program = new Command();
|
|
2887
|
-
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.
|
|
3297
|
+
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.18").option("-k, --api-key <key>", "API key (overrides stored key)").option(
|
|
2888
3298
|
"-u, --base-url <url>",
|
|
2889
3299
|
"API base URL (default: https://tunedtensor.com)"
|
|
2890
3300
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|