@tuned-tensor/cli 0.4.17 → 0.4.19
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 +20 -2
- package/dist/index.js +400 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ tt init --name "Customer Support Bot" --model Qwen/Qwen3.5-2B
|
|
|
34
34
|
# Edit tunedtensor.json, then:
|
|
35
35
|
tt eval
|
|
36
36
|
tt push
|
|
37
|
+
tt runs estimate <spec-id>
|
|
37
38
|
tt runs start <spec-id>
|
|
38
39
|
tt runs watch <run-id>
|
|
39
40
|
```
|
|
@@ -55,9 +56,26 @@ tt models base
|
|
|
55
56
|
tt balance
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
Export a model to GGUF and package it for Ollama (so it's pluggable like any
|
|
60
|
+
other local model, e.g. in OpenClaw via Ollama's native `/api/chat`):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Convert + quantize to GGUF, write a Modelfile, and run `ollama create`
|
|
64
|
+
tt models export <model-id> --format gguf --quant q4_k_m --ollama
|
|
65
|
+
|
|
66
|
+
# Inspect the planned llama.cpp / ollama commands without running them
|
|
67
|
+
tt models export <model-id> --quant q8_0 --ollama --print-command
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This wraps llama.cpp's `convert_hf_to_gguf.py` + `llama-quantize` and Ollama's
|
|
71
|
+
`ollama create`. Point `tt` at your llama.cpp checkout with `--llama-cpp <dir>`
|
|
72
|
+
(or `--convert-script` / `--quantize-bin`); with `--ollama` the behaviour spec's
|
|
73
|
+
system prompt is embedded as the Modelfile `SYSTEM` block.
|
|
74
|
+
|
|
58
75
|
For the full command reference, including dataset-backed runs, long-example
|
|
59
|
-
policies,
|
|
60
|
-
configuration, and billing, see the
|
|
76
|
+
policies, eval token budgets, preflight run estimates, continued fine-tuning,
|
|
77
|
+
evaluation caps, local model serving, configuration, and billing, see the
|
|
78
|
+
[CLI docs](https://tunedtensor.com/docs/cli).
|
|
61
79
|
|
|
62
80
|
## Development
|
|
63
81
|
|
package/dist/index.js
CHANGED
|
@@ -617,6 +617,29 @@ function formatMinutes(minutes) {
|
|
|
617
617
|
if (minutes < 60) return `${Math.max(1, Math.round(minutes))}m`;
|
|
618
618
|
return `${(minutes / 60).toFixed(1)}h`;
|
|
619
619
|
}
|
|
620
|
+
function formatCents(cents) {
|
|
621
|
+
const dollars = cents / 100;
|
|
622
|
+
if (Math.abs(dollars) >= 100) return `$${dollars.toFixed(0)}`;
|
|
623
|
+
return `$${dollars.toFixed(2)}`;
|
|
624
|
+
}
|
|
625
|
+
function formatEstimateRange(estimate) {
|
|
626
|
+
const duration = estimate.duration;
|
|
627
|
+
return `${formatMinutes(duration.estimated_minutes)} (${formatMinutes(duration.range_minutes.low)}-${formatMinutes(duration.range_minutes.high)})`;
|
|
628
|
+
}
|
|
629
|
+
function printRunEstimate(estimate) {
|
|
630
|
+
printDetail([
|
|
631
|
+
["Estimated Time", formatEstimateRange(estimate)],
|
|
632
|
+
["Confidence", estimate.duration.confidence],
|
|
633
|
+
["History Samples", String(estimate.duration.sample_count)],
|
|
634
|
+
["Basis", estimate.duration.basis.replaceAll("_", " ")],
|
|
635
|
+
["Estimated Cost", formatCents(estimate.estimated_cost_cents)],
|
|
636
|
+
["Training Tokens", `${(estimate.estimated_training_tokens / 1e3).toFixed(1)}k`],
|
|
637
|
+
["Epochs", String(estimate.estimated_epochs)]
|
|
638
|
+
]);
|
|
639
|
+
console.log(
|
|
640
|
+
"\nDuration is a rough historical range. Final cost uses provider-reported training tokens."
|
|
641
|
+
);
|
|
642
|
+
}
|
|
620
643
|
function formatEpochProgress(diagnostics) {
|
|
621
644
|
const curve = diagnostics.training.curve;
|
|
622
645
|
if (curve.latest_epoch == null) return void 0;
|
|
@@ -722,6 +745,46 @@ function printEvalOutputDiagnostics(diagnostics) {
|
|
|
722
745
|
}
|
|
723
746
|
}
|
|
724
747
|
}
|
|
748
|
+
function addRunConfigurationOptions(command) {
|
|
749
|
+
return command.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");
|
|
750
|
+
}
|
|
751
|
+
async function buildRunRequestBody(cmdOpts, opts) {
|
|
752
|
+
const body = {};
|
|
753
|
+
if (cmdOpts.augment === false) body.augment = false;
|
|
754
|
+
if (cmdOpts.dataset) body.dataset_id = await resolveDatasetId(String(cmdOpts.dataset), opts);
|
|
755
|
+
if (cmdOpts.parentModel) {
|
|
756
|
+
body.parent_model_id = await resolveModelId(String(cmdOpts.parentModel), opts);
|
|
757
|
+
}
|
|
758
|
+
const splitRatioOptions = [
|
|
759
|
+
cmdOpts.trainRatio,
|
|
760
|
+
cmdOpts.validationRatio,
|
|
761
|
+
cmdOpts.testRatio
|
|
762
|
+
];
|
|
763
|
+
if (splitRatioOptions.some((value) => value != null)) {
|
|
764
|
+
body.split_ratios = {
|
|
765
|
+
train: cmdOpts.trainRatio != null ? Number(cmdOpts.trainRatio) : 0.8,
|
|
766
|
+
validation: cmdOpts.validationRatio != null ? Number(cmdOpts.validationRatio) : 0.1,
|
|
767
|
+
test: cmdOpts.testRatio != null ? Number(cmdOpts.testRatio) : 0.1
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const hp = {};
|
|
771
|
+
if (cmdOpts.epochs) hp.n_epochs = Number(cmdOpts.epochs);
|
|
772
|
+
if (cmdOpts.lr) hp.learning_rate = Number(cmdOpts.lr);
|
|
773
|
+
if (cmdOpts.batchSize) hp.batch_size = Number(cmdOpts.batchSize);
|
|
774
|
+
if (cmdOpts.loraRank) hp.lora_rank = Number(cmdOpts.loraRank);
|
|
775
|
+
if (cmdOpts.loraAlpha) hp.lora_alpha = Number(cmdOpts.loraAlpha);
|
|
776
|
+
if (cmdOpts.maxEvalExamples) hp.max_eval_examples = Number(cmdOpts.maxEvalExamples);
|
|
777
|
+
if (cmdOpts.maxTestEvalExamples) hp.max_test_eval_examples = Number(cmdOpts.maxTestEvalExamples);
|
|
778
|
+
if (cmdOpts.longExamples) hp.long_examples = parseLongExamplesPolicy(String(cmdOpts.longExamples));
|
|
779
|
+
if (cmdOpts.maxSeqLength) hp.max_seq_length = Number(cmdOpts.maxSeqLength);
|
|
780
|
+
if (cmdOpts.maxOutputTokens) hp.max_output_tokens = Number(cmdOpts.maxOutputTokens);
|
|
781
|
+
if (cmdOpts.evalReservedOutputTokens) {
|
|
782
|
+
hp.eval_reserved_output_tokens = Number(cmdOpts.evalReservedOutputTokens);
|
|
783
|
+
}
|
|
784
|
+
if (cmdOpts.llmJudge === false) body.use_llm_judge = false;
|
|
785
|
+
if (Object.keys(hp).length) body.hyperparameters = hp;
|
|
786
|
+
return body;
|
|
787
|
+
}
|
|
725
788
|
function registerRunsCommands(parent) {
|
|
726
789
|
const runs = parent.command("runs").description("Manage runs");
|
|
727
790
|
runs.command("list").description("List runs").option("-s, --spec <id>", "Filter by spec ID (full UUID or 8+ char prefix)").option("-p, --page <n>", "Page number", "1").option("--per-page <n>", "Results per page", "20").action(async (cmdOpts) => {
|
|
@@ -801,38 +864,25 @@ Eval Results (${data._evals.length}):`);
|
|
|
801
864
|
);
|
|
802
865
|
}
|
|
803
866
|
});
|
|
804
|
-
|
|
867
|
+
addRunConfigurationOptions(
|
|
868
|
+
runs.command("estimate").description("Estimate run cost and duration before starting").argument("<spec-id>", "Behaviour spec ID (full UUID or 8+ char prefix)")
|
|
869
|
+
).action(async (specId, cmdOpts) => {
|
|
805
870
|
const opts = parent.opts();
|
|
806
|
-
const body =
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
body
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
test: cmdOpts.testRatio != null ? Number(cmdOpts.testRatio) : 0.1
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
const hp = {};
|
|
825
|
-
if (cmdOpts.epochs) hp.n_epochs = Number(cmdOpts.epochs);
|
|
826
|
-
if (cmdOpts.lr) hp.learning_rate = Number(cmdOpts.lr);
|
|
827
|
-
if (cmdOpts.batchSize) hp.batch_size = Number(cmdOpts.batchSize);
|
|
828
|
-
if (cmdOpts.loraRank) hp.lora_rank = Number(cmdOpts.loraRank);
|
|
829
|
-
if (cmdOpts.loraAlpha) hp.lora_alpha = Number(cmdOpts.loraAlpha);
|
|
830
|
-
if (cmdOpts.maxEvalExamples) hp.max_eval_examples = Number(cmdOpts.maxEvalExamples);
|
|
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.llmJudge === false) body.use_llm_judge = false;
|
|
835
|
-
if (Object.keys(hp).length) body.hyperparameters = hp;
|
|
871
|
+
const body = await buildRunRequestBody(cmdOpts, opts);
|
|
872
|
+
const fullSpecId = await resolveSpecId(specId, opts);
|
|
873
|
+
const { data } = await post(
|
|
874
|
+
`/behavior-specs/${fullSpecId}/runs/estimate`,
|
|
875
|
+
body,
|
|
876
|
+
opts
|
|
877
|
+
);
|
|
878
|
+
if (isJsonMode()) return printJson(data);
|
|
879
|
+
printRunEstimate(data);
|
|
880
|
+
});
|
|
881
|
+
addRunConfigurationOptions(
|
|
882
|
+
runs.command("start").description("Start a new run for a behaviour spec").argument("<spec-id>", "Behaviour spec ID (full UUID or 8+ char prefix)")
|
|
883
|
+
).action(async (specId, cmdOpts) => {
|
|
884
|
+
const opts = parent.opts();
|
|
885
|
+
const body = await buildRunRequestBody(cmdOpts, opts);
|
|
836
886
|
const fullSpecId = await resolveSpecId(specId, opts);
|
|
837
887
|
const { data } = await post(
|
|
838
888
|
`/behavior-specs/${fullSpecId}/runs`,
|
|
@@ -2370,6 +2420,174 @@ function printServeCommand(command, args, env, managed) {
|
|
|
2370
2420
|
}
|
|
2371
2421
|
printDetail(fields);
|
|
2372
2422
|
}
|
|
2423
|
+
var CONVERT_NATIVE_OUTTYPES = /* @__PURE__ */ new Set([
|
|
2424
|
+
"f32",
|
|
2425
|
+
"f16",
|
|
2426
|
+
"bf16",
|
|
2427
|
+
"q8_0",
|
|
2428
|
+
"tq1_0",
|
|
2429
|
+
"tq2_0"
|
|
2430
|
+
]);
|
|
2431
|
+
var QUANTIZE_TYPES = /* @__PURE__ */ new Set([
|
|
2432
|
+
"q4_0",
|
|
2433
|
+
"q4_1",
|
|
2434
|
+
"q5_0",
|
|
2435
|
+
"q5_1",
|
|
2436
|
+
"q2_k",
|
|
2437
|
+
"q2_k_s",
|
|
2438
|
+
"q3_k_s",
|
|
2439
|
+
"q3_k_m",
|
|
2440
|
+
"q3_k_l",
|
|
2441
|
+
"q4_k_s",
|
|
2442
|
+
"q4_k_m",
|
|
2443
|
+
"q5_k_s",
|
|
2444
|
+
"q5_k_m",
|
|
2445
|
+
"q6_k",
|
|
2446
|
+
"iq1_s",
|
|
2447
|
+
"iq1_m",
|
|
2448
|
+
"iq2_xxs",
|
|
2449
|
+
"iq2_xs",
|
|
2450
|
+
"iq2_s",
|
|
2451
|
+
"iq2_m",
|
|
2452
|
+
"iq3_xxs",
|
|
2453
|
+
"iq3_s",
|
|
2454
|
+
"iq3_m",
|
|
2455
|
+
"iq4_nl",
|
|
2456
|
+
"iq4_xs"
|
|
2457
|
+
]);
|
|
2458
|
+
function planQuant(quant) {
|
|
2459
|
+
const lower = quant.trim().toLowerCase();
|
|
2460
|
+
if (!lower) {
|
|
2461
|
+
throw new Error("--quant requires a value (e.g. q4_k_m, q8_0, f16).");
|
|
2462
|
+
}
|
|
2463
|
+
if (CONVERT_NATIVE_OUTTYPES.has(lower)) {
|
|
2464
|
+
return { quant: lower, requiresQuantize: false, convertOuttype: lower };
|
|
2465
|
+
}
|
|
2466
|
+
if (QUANTIZE_TYPES.has(lower)) {
|
|
2467
|
+
return {
|
|
2468
|
+
quant: lower,
|
|
2469
|
+
requiresQuantize: true,
|
|
2470
|
+
convertOuttype: "f16",
|
|
2471
|
+
quantizeType: lower.toUpperCase()
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
const supported = [...CONVERT_NATIVE_OUTTYPES, ...QUANTIZE_TYPES].sort().join(", ");
|
|
2475
|
+
throw new Error(`Unsupported --quant "${quant}". Supported types: ${supported}.`);
|
|
2476
|
+
}
|
|
2477
|
+
function ollamaSlug(name) {
|
|
2478
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
2479
|
+
return slug || "model";
|
|
2480
|
+
}
|
|
2481
|
+
function firstExisting(candidates) {
|
|
2482
|
+
for (const candidate of candidates) {
|
|
2483
|
+
if (candidate && existsSync4(candidate) && statSync3(candidate).isFile()) {
|
|
2484
|
+
return resolve2(candidate);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2489
|
+
function llamaCppDir(explicit) {
|
|
2490
|
+
return explicit || process.env.LLAMA_CPP_DIR || process.env.LLAMA_CPP_HOME || void 0;
|
|
2491
|
+
}
|
|
2492
|
+
function resolveConvertScript(opts, required) {
|
|
2493
|
+
if (opts.convertScript) {
|
|
2494
|
+
const resolved = resolve2(opts.convertScript);
|
|
2495
|
+
if (required && !existsSync4(resolved)) {
|
|
2496
|
+
throw new Error(`Conversion script not found: ${opts.convertScript}`);
|
|
2497
|
+
}
|
|
2498
|
+
return resolved;
|
|
2499
|
+
}
|
|
2500
|
+
const dir = llamaCppDir(opts.llamaCpp);
|
|
2501
|
+
const candidates = dir ? [join2(dir, "convert_hf_to_gguf.py"), join2(dir, "convert-hf-to-gguf.py")] : [];
|
|
2502
|
+
const found = firstExisting(candidates);
|
|
2503
|
+
if (found) return found;
|
|
2504
|
+
if (required) {
|
|
2505
|
+
throw new Error(
|
|
2506
|
+
"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."
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
return dir ? join2(dir, "convert_hf_to_gguf.py") : "convert_hf_to_gguf.py";
|
|
2510
|
+
}
|
|
2511
|
+
function resolveQuantizeBin(opts, required) {
|
|
2512
|
+
if (opts.quantizeBin) {
|
|
2513
|
+
const resolved = resolve2(opts.quantizeBin);
|
|
2514
|
+
if (required && !existsSync4(resolved)) {
|
|
2515
|
+
throw new Error(`llama-quantize binary not found: ${opts.quantizeBin}`);
|
|
2516
|
+
}
|
|
2517
|
+
return resolved;
|
|
2518
|
+
}
|
|
2519
|
+
const dir = llamaCppDir(opts.llamaCpp);
|
|
2520
|
+
if (dir) {
|
|
2521
|
+
const candidates = [
|
|
2522
|
+
join2(dir, "build", "bin", "llama-quantize"),
|
|
2523
|
+
join2(dir, "build", "bin", "quantize"),
|
|
2524
|
+
join2(dir, "llama-quantize"),
|
|
2525
|
+
join2(dir, "quantize")
|
|
2526
|
+
];
|
|
2527
|
+
const found = firstExisting(candidates);
|
|
2528
|
+
if (found) return found;
|
|
2529
|
+
}
|
|
2530
|
+
if (required && !commandExists("llama-quantize")) {
|
|
2531
|
+
throw new Error(
|
|
2532
|
+
"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."
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
return "llama-quantize";
|
|
2536
|
+
}
|
|
2537
|
+
function buildModelfile(ggufBasename, systemPrompt) {
|
|
2538
|
+
const lines = [`FROM ./${ggufBasename}`];
|
|
2539
|
+
if (systemPrompt.trim()) {
|
|
2540
|
+
const escaped = systemPrompt.trim().replace(/"""/g, '\\"\\"\\"');
|
|
2541
|
+
lines.push("", `SYSTEM """${escaped}"""`);
|
|
2542
|
+
}
|
|
2543
|
+
return lines.join("\n") + "\n";
|
|
2544
|
+
}
|
|
2545
|
+
function buildExportStep(name, command, args) {
|
|
2546
|
+
return {
|
|
2547
|
+
name,
|
|
2548
|
+
command,
|
|
2549
|
+
args,
|
|
2550
|
+
command_line: [command, ...args].map(quoteCommandPart).join(" ")
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
function printExportPlan(plan) {
|
|
2554
|
+
if (isJsonMode()) return printJson(plan);
|
|
2555
|
+
printDetail([
|
|
2556
|
+
["Model", `${plan.model.name} (${plan.model.source})`],
|
|
2557
|
+
["Format", plan.format],
|
|
2558
|
+
["Quant", plan.quant],
|
|
2559
|
+
["Output dir", plan.output_dir],
|
|
2560
|
+
["GGUF", plan.gguf_path]
|
|
2561
|
+
]);
|
|
2562
|
+
console.log("\nPlanned steps:");
|
|
2563
|
+
for (const step of plan.steps) {
|
|
2564
|
+
console.log(` ${step.name}: ${step.command_line}`);
|
|
2565
|
+
}
|
|
2566
|
+
if (plan.ollama) {
|
|
2567
|
+
console.log(`
|
|
2568
|
+
Modelfile (${plan.ollama.modelfile_path}):`);
|
|
2569
|
+
console.log(plan.ollama.modelfile.replace(/^/gm, " "));
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
function printOpenClawHints(ollamaName) {
|
|
2573
|
+
console.log("\nUse it with OpenClaw via Ollama's native /api/chat (not /v1):");
|
|
2574
|
+
console.log(
|
|
2575
|
+
[
|
|
2576
|
+
" {",
|
|
2577
|
+
" models: { providers: { ollama: {",
|
|
2578
|
+
' api: "ollama",',
|
|
2579
|
+
' baseUrl: "http://127.0.0.1:11434",',
|
|
2580
|
+
` models: [{ id: "${ollamaName}" }]`,
|
|
2581
|
+
" }}}",
|
|
2582
|
+
" }"
|
|
2583
|
+
].join("\n")
|
|
2584
|
+
);
|
|
2585
|
+
console.log("\nThen run it through the infer surface:");
|
|
2586
|
+
console.log(
|
|
2587
|
+
` openclaw infer model run --local --model ollama/${ollamaName} \\
|
|
2588
|
+
--prompt "<payload>" --json`
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2373
2591
|
function registerModelsCommands(parent) {
|
|
2374
2592
|
const models = parent.command("models").description("Manage fine-tuned models");
|
|
2375
2593
|
models.command("base").description("List supported base models").action(async () => {
|
|
@@ -2444,6 +2662,149 @@ function registerModelsCommands(parent) {
|
|
|
2444
2662
|
const size = bytes == null ? "" : ` (${bytes} bytes)`;
|
|
2445
2663
|
printSuccess(`Model downloaded to ${outputPath}${size}`);
|
|
2446
2664
|
});
|
|
2665
|
+
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) => {
|
|
2666
|
+
const opts = parent.opts();
|
|
2667
|
+
const format = String(cmdOpts.format).toLowerCase();
|
|
2668
|
+
if (format !== "gguf") {
|
|
2669
|
+
throw new Error(`Unsupported --format "${cmdOpts.format}". Only "gguf" is supported.`);
|
|
2670
|
+
}
|
|
2671
|
+
const quantPlan = planQuant(String(cmdOpts.quant));
|
|
2672
|
+
const printOnly = Boolean(cmdOpts.printCommand);
|
|
2673
|
+
const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
|
|
2674
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
2675
|
+
const serveTarget = await resolveServeTarget(
|
|
2676
|
+
target,
|
|
2677
|
+
opts,
|
|
2678
|
+
cacheDir,
|
|
2679
|
+
Boolean(cmdOpts.forceDownload)
|
|
2680
|
+
);
|
|
2681
|
+
const slug = ollamaSlug(serveTarget.modelName);
|
|
2682
|
+
const outputDir = resolve2(cmdOpts.output || join2(process.cwd(), `${slug}-gguf`));
|
|
2683
|
+
const finalPath = join2(outputDir, `${slug}.${quantPlan.quant}.gguf`);
|
|
2684
|
+
const intermediatePath = quantPlan.requiresQuantize ? join2(outputDir, `${slug}.f16.gguf`) : void 0;
|
|
2685
|
+
const convertScript = resolveConvertScript(cmdOpts, !printOnly);
|
|
2686
|
+
const convertOutfile = intermediatePath ?? finalPath;
|
|
2687
|
+
const steps = [
|
|
2688
|
+
buildExportStep("convert", cmdOpts.python, [
|
|
2689
|
+
convertScript,
|
|
2690
|
+
serveTarget.modelPath,
|
|
2691
|
+
"--outfile",
|
|
2692
|
+
convertOutfile,
|
|
2693
|
+
"--outtype",
|
|
2694
|
+
quantPlan.convertOuttype
|
|
2695
|
+
])
|
|
2696
|
+
];
|
|
2697
|
+
let quantizeBin;
|
|
2698
|
+
if (quantPlan.requiresQuantize) {
|
|
2699
|
+
quantizeBin = resolveQuantizeBin(cmdOpts, !printOnly);
|
|
2700
|
+
steps.push(
|
|
2701
|
+
buildExportStep("quantize", quantizeBin, [
|
|
2702
|
+
intermediatePath,
|
|
2703
|
+
finalPath,
|
|
2704
|
+
quantPlan.quantizeType
|
|
2705
|
+
])
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
const wantOllama = Boolean(cmdOpts.ollama);
|
|
2709
|
+
const ollamaName = cmdOpts.ollamaName || `tt-${slug}`;
|
|
2710
|
+
let ollamaInfo;
|
|
2711
|
+
let modelfileContent = "";
|
|
2712
|
+
let modelfilePath = "";
|
|
2713
|
+
if (wantOllama) {
|
|
2714
|
+
let systemPrompt = "";
|
|
2715
|
+
if (cmdOpts.specPrompt !== false) {
|
|
2716
|
+
const specPath = findSpecPath(cmdOpts.spec, serveTarget.modelPath);
|
|
2717
|
+
if (cmdOpts.spec && !specPath) {
|
|
2718
|
+
throw new Error(`Spec file not found: ${cmdOpts.spec}`);
|
|
2719
|
+
}
|
|
2720
|
+
if (specPath) systemPrompt = loadSystemPromptFromSpec(specPath);
|
|
2721
|
+
}
|
|
2722
|
+
modelfilePath = join2(outputDir, "Modelfile");
|
|
2723
|
+
modelfileContent = buildModelfile(basename2(finalPath), systemPrompt);
|
|
2724
|
+
const createModel = cmdOpts.ollamaCreate !== false;
|
|
2725
|
+
if (createModel) {
|
|
2726
|
+
steps.push(
|
|
2727
|
+
buildExportStep("ollama-create", cmdOpts.ollamaBin, [
|
|
2728
|
+
"create",
|
|
2729
|
+
ollamaName,
|
|
2730
|
+
"-f",
|
|
2731
|
+
modelfilePath
|
|
2732
|
+
])
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
ollamaInfo = {
|
|
2736
|
+
name: ollamaName,
|
|
2737
|
+
modelfile_path: modelfilePath,
|
|
2738
|
+
modelfile: modelfileContent,
|
|
2739
|
+
create: createModel
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
const plan = {
|
|
2743
|
+
format: "gguf",
|
|
2744
|
+
quant: quantPlan.quant,
|
|
2745
|
+
model: {
|
|
2746
|
+
name: serveTarget.modelName,
|
|
2747
|
+
path: serveTarget.modelPath,
|
|
2748
|
+
source: serveTarget.source,
|
|
2749
|
+
id: serveTarget.modelId
|
|
2750
|
+
},
|
|
2751
|
+
output_dir: outputDir,
|
|
2752
|
+
gguf_path: finalPath,
|
|
2753
|
+
intermediate_path: intermediatePath,
|
|
2754
|
+
steps,
|
|
2755
|
+
ollama: ollamaInfo
|
|
2756
|
+
};
|
|
2757
|
+
if (printOnly) return printExportPlan(plan);
|
|
2758
|
+
if (existsSync4(finalPath) && !cmdOpts.force) {
|
|
2759
|
+
throw new Error(`Output already exists: ${finalPath}. Use --force to overwrite.`);
|
|
2760
|
+
}
|
|
2761
|
+
mkdirSync3(outputDir, { recursive: true });
|
|
2762
|
+
const runStep = (command, args) => execFileSync(command, args, { stdio: "inherit" });
|
|
2763
|
+
if (!isJsonMode()) printSuccess(`Converting ${serveTarget.modelName} \u2192 ${quantPlan.convertOuttype} GGUF`);
|
|
2764
|
+
runStep(cmdOpts.python, [
|
|
2765
|
+
convertScript,
|
|
2766
|
+
serveTarget.modelPath,
|
|
2767
|
+
"--outfile",
|
|
2768
|
+
convertOutfile,
|
|
2769
|
+
"--outtype",
|
|
2770
|
+
quantPlan.convertOuttype
|
|
2771
|
+
]);
|
|
2772
|
+
if (quantPlan.requiresQuantize && quantizeBin) {
|
|
2773
|
+
if (!isJsonMode()) printSuccess(`Quantizing \u2192 ${quantPlan.quantizeType}`);
|
|
2774
|
+
runStep(quantizeBin, [
|
|
2775
|
+
intermediatePath,
|
|
2776
|
+
finalPath,
|
|
2777
|
+
quantPlan.quantizeType
|
|
2778
|
+
]);
|
|
2779
|
+
if (!cmdOpts.keepIntermediate && intermediatePath && existsSync4(intermediatePath)) {
|
|
2780
|
+
rmSync(intermediatePath, { force: true });
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
if (ollamaInfo) {
|
|
2784
|
+
writeFileSync3(modelfilePath, modelfileContent, "utf8");
|
|
2785
|
+
if (!isJsonMode()) printSuccess(`Wrote Modelfile to ${modelfilePath}`);
|
|
2786
|
+
if (ollamaInfo.create) {
|
|
2787
|
+
if (!isJsonMode()) printSuccess(`Creating Ollama model ${ollamaName}`);
|
|
2788
|
+
runStep(cmdOpts.ollamaBin, ["create", ollamaName, "-f", modelfilePath]);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
if (isJsonMode()) {
|
|
2792
|
+
return printJson({
|
|
2793
|
+
format: "gguf",
|
|
2794
|
+
quant: quantPlan.quant,
|
|
2795
|
+
output_dir: outputDir,
|
|
2796
|
+
gguf_path: finalPath,
|
|
2797
|
+
ollama: ollamaInfo ? { name: ollamaName, modelfile_path: modelfilePath, created: ollamaInfo.create } : void 0
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
printSuccess(`GGUF written to ${finalPath}`);
|
|
2801
|
+
if (ollamaInfo?.create) {
|
|
2802
|
+
console.log(`Run it: ollama run ${ollamaName}`);
|
|
2803
|
+
printOpenClawHints(ollamaName);
|
|
2804
|
+
} else if (ollamaInfo) {
|
|
2805
|
+
console.log(`Create the Ollama model: ollama create ${ollamaName} -f ${modelfilePath}`);
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2447
2808
|
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) => {
|
|
2448
2809
|
const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
|
|
2449
2810
|
const venvDir = runtimeDir(cacheDir);
|
|
@@ -2612,15 +2973,15 @@ var KIND_LABELS = {
|
|
|
2612
2973
|
refund: "Refund",
|
|
2613
2974
|
adjustment: "Adjustment"
|
|
2614
2975
|
};
|
|
2615
|
-
function
|
|
2976
|
+
function formatCents2(cents) {
|
|
2616
2977
|
const sign = cents < 0 ? "-" : "";
|
|
2617
2978
|
const abs = Math.abs(cents);
|
|
2618
2979
|
return `${sign}$${(abs / 100).toFixed(2)}`;
|
|
2619
2980
|
}
|
|
2620
2981
|
function formatSigned(cents) {
|
|
2621
|
-
if (cents > 0) return chalk3.green(`+${
|
|
2622
|
-
if (cents < 0) return chalk3.red(
|
|
2623
|
-
return
|
|
2982
|
+
if (cents > 0) return chalk3.green(`+${formatCents2(cents)}`);
|
|
2983
|
+
if (cents < 0) return chalk3.red(formatCents2(cents));
|
|
2984
|
+
return formatCents2(cents);
|
|
2624
2985
|
}
|
|
2625
2986
|
function registerBalanceCommands(parent) {
|
|
2626
2987
|
parent.command("balance").description("Show credit balance and recent transactions").option("-n, --limit <n>", "Number of transactions to show (default 10)", "10").action(async (options) => {
|
|
@@ -2634,10 +2995,10 @@ function registerBalanceCommands(parent) {
|
|
|
2634
2995
|
return printJson({ balance, transactions });
|
|
2635
2996
|
}
|
|
2636
2997
|
const lowBalance = balance.balance_cents < 100;
|
|
2637
|
-
const balanceLine = lowBalance ? chalk3.red(
|
|
2998
|
+
const balanceLine = lowBalance ? chalk3.red(formatCents2(balance.balance_cents)) + chalk3.dim(" (low)") : chalk3.bold.green(formatCents2(balance.balance_cents));
|
|
2638
2999
|
printDetail([
|
|
2639
3000
|
["Credits", balanceLine],
|
|
2640
|
-
["Lifetime top-ups",
|
|
3001
|
+
["Lifetime top-ups", formatCents2(balance.lifetime_topup_cents)]
|
|
2641
3002
|
]);
|
|
2642
3003
|
if (transactions && transactions.length > 0) {
|
|
2643
3004
|
console.log(`
|
|
@@ -2648,7 +3009,7 @@ ${chalk3.bold("Recent transactions")}`);
|
|
|
2648
3009
|
formatDate(row.created_at),
|
|
2649
3010
|
KIND_LABELS[row.kind] ?? row.kind,
|
|
2650
3011
|
formatSigned(row.amount_cents),
|
|
2651
|
-
|
|
3012
|
+
formatCents2(row.balance_after_cents),
|
|
2652
3013
|
row.reference_id ? shortId(row.reference_id) : chalk3.dim("\u2014")
|
|
2653
3014
|
])
|
|
2654
3015
|
);
|
|
@@ -2979,7 +3340,7 @@ function registerPushCommand(parent) {
|
|
|
2979
3340
|
|
|
2980
3341
|
// src/index.ts
|
|
2981
3342
|
var program = new Command();
|
|
2982
|
-
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.
|
|
3343
|
+
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.19").option("-k, --api-key <key>", "API key (overrides stored key)").option(
|
|
2983
3344
|
"-u, --base-url <url>",
|
|
2984
3345
|
"API base URL (default: https://tunedtensor.com)"
|
|
2985
3346
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|