@tuned-tensor/cli 0.4.15 → 0.4.17

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 CHANGED
@@ -38,6 +38,12 @@ tt runs start <spec-id>
38
38
  tt runs watch <run-id>
39
39
  ```
40
40
 
41
+ To continue training from a completed fine-tuned model artifact:
42
+
43
+ ```bash
44
+ tt runs start <spec-id> --parent-model <model-id>
45
+ ```
46
+
41
47
  Useful discovery commands:
42
48
 
43
49
  ```bash
@@ -49,9 +55,9 @@ tt models base
49
55
  tt balance
50
56
  ```
51
57
 
52
- For the full command reference, including dataset-backed runs, evaluation caps,
53
- local model serving, configuration, and billing, see the
54
- [CLI docs](https://tunedtensor.com/docs/cli).
58
+ For the full command reference, including dataset-backed runs, long-example
59
+ policies, continued fine-tuning, evaluation caps, local model serving,
60
+ configuration, and billing, see the [CLI docs](https://tunedtensor.com/docs/cli).
55
61
 
56
62
  ## Development
57
63
 
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,11 +801,14 @@ 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("--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").action(async (specId, cmdOpts) => {
795
805
  const opts = parent.opts();
796
806
  const body = {};
797
807
  if (cmdOpts.augment === false) body.augment = false;
798
808
  if (cmdOpts.dataset) body.dataset_id = await resolveDatasetId(cmdOpts.dataset, opts);
809
+ if (cmdOpts.parentModel) {
810
+ body.parent_model_id = await resolveModelId(cmdOpts.parentModel, opts);
811
+ }
799
812
  const splitRatioOptions = [
800
813
  cmdOpts.trainRatio,
801
814
  cmdOpts.validationRatio,
@@ -816,6 +829,8 @@ Eval Results (${data._evals.length}):`);
816
829
  if (cmdOpts.loraAlpha) hp.lora_alpha = Number(cmdOpts.loraAlpha);
817
830
  if (cmdOpts.maxEvalExamples) hp.max_eval_examples = Number(cmdOpts.maxEvalExamples);
818
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);
819
834
  if (cmdOpts.llmJudge === false) body.use_llm_judge = false;
820
835
  if (Object.keys(hp).length) body.hyperparameters = hp;
821
836
  const fullSpecId = await resolveSpecId(specId, opts);
@@ -2781,11 +2796,90 @@ function validateSpec(spec) {
2781
2796
  message: violations.length === 0 ? void 0 : violations.join("; ")
2782
2797
  });
2783
2798
  }
2799
+ const evalCaseErrors = validateEvalCases(spec);
2800
+ checks.push({
2801
+ name: "Eval cases valid",
2802
+ passed: evalCaseErrors.length === 0,
2803
+ message: evalCaseErrors.length === 0 ? spec.eval_cases?.length ? `${spec.eval_cases.length} executable eval case(s)` : void 0 : evalCaseErrors.join("; ")
2804
+ });
2784
2805
  return {
2785
2806
  valid: checks.every((c) => c.passed),
2786
2807
  checks
2787
2808
  };
2788
2809
  }
2810
+ function validateEvalCases(spec) {
2811
+ const errors = [];
2812
+ const cases = spec.eval_cases ?? [];
2813
+ for (const [caseIndex, evalCase] of cases.entries()) {
2814
+ const label = `eval_cases[${caseIndex}]`;
2815
+ if (!evalCase || typeof evalCase !== "object") {
2816
+ errors.push(`${label} must be an object`);
2817
+ continue;
2818
+ }
2819
+ if (typeof evalCase.input !== "string" || evalCase.input.trim().length === 0) {
2820
+ errors.push(`${label}.input must be a non-empty string`);
2821
+ }
2822
+ if (evalCase.runtime !== "python") {
2823
+ errors.push(`${label}.runtime must be "python"`);
2824
+ }
2825
+ if (!Array.isArray(evalCase.tests) || evalCase.tests.length === 0) {
2826
+ errors.push(`${label}.tests must contain at least one test`);
2827
+ continue;
2828
+ }
2829
+ for (const [testIndex, test] of evalCase.tests.entries()) {
2830
+ const testLabel = `${label}.tests[${testIndex}]`;
2831
+ if (!test || typeof test !== "object") {
2832
+ errors.push(`${testLabel} must be an object`);
2833
+ continue;
2834
+ }
2835
+ if (test.name !== void 0 && (typeof test.name !== "string" || test.name.trim().length === 0)) {
2836
+ errors.push(`${testLabel}.name must be a non-empty string when provided`);
2837
+ }
2838
+ if (test.args !== void 0 && (!Array.isArray(test.args) || !test.args.every((arg) => typeof arg === "string"))) {
2839
+ errors.push(`${testLabel}.args must be an array of strings`);
2840
+ }
2841
+ if (test.stdin !== void 0 && typeof test.stdin !== "string") {
2842
+ errors.push(`${testLabel}.stdin must be a string`);
2843
+ }
2844
+ 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)) {
2845
+ errors.push(`${testLabel}.expected_exit_code must be an integer between 0 and 255`);
2846
+ }
2847
+ if (test.expected_stdout !== void 0 && typeof test.expected_stdout !== "string") {
2848
+ errors.push(`${testLabel}.expected_stdout must be a string`);
2849
+ }
2850
+ validateFiles(test.files, `${testLabel}.files`, errors);
2851
+ validateFiles(test.expected_files, `${testLabel}.expected_files`, errors);
2852
+ }
2853
+ }
2854
+ return errors;
2855
+ }
2856
+ function validateFiles(files, label, errors) {
2857
+ if (files === void 0) return;
2858
+ if (!Array.isArray(files)) {
2859
+ errors.push(`${label} must be an array`);
2860
+ return;
2861
+ }
2862
+ for (const [index, file] of files.entries()) {
2863
+ const fileLabel = `${label}[${index}]`;
2864
+ if (!file || typeof file !== "object") {
2865
+ errors.push(`${fileLabel} must be an object`);
2866
+ continue;
2867
+ }
2868
+ const row = file;
2869
+ if (typeof row.path !== "string" || !isSafeRelativePath(row.path)) {
2870
+ errors.push(`${fileLabel}.path must be a safe relative path`);
2871
+ }
2872
+ if (row.content !== void 0 && typeof row.content !== "string") {
2873
+ errors.push(`${fileLabel}.content must be a string`);
2874
+ }
2875
+ }
2876
+ }
2877
+ function isSafeRelativePath(value) {
2878
+ if (value.length === 0 || value.length > 240) return false;
2879
+ if (value.includes("\0") || value.startsWith("/")) return false;
2880
+ const normalized = value.replace(/\\/g, "/");
2881
+ return !normalized.split("/").some((part) => part === "" || part === "." || part === "..");
2882
+ }
2789
2883
  function checkExamplesAgainstConstraints(spec) {
2790
2884
  const violations = [];
2791
2885
  for (const [i, ex] of spec.examples.entries()) {
@@ -2859,7 +2953,11 @@ function registerPushCommand(parent) {
2859
2953
  const opts = parent.opts();
2860
2954
  const filePath = resolve3(cmdOpts.file);
2861
2955
  const spec = loadSpec(cmdOpts.file);
2862
- const { id, eval_cases, ...rawBody } = spec;
2956
+ const evalCaseErrors = validateEvalCases(spec);
2957
+ if (evalCaseErrors.length > 0) {
2958
+ throw new Error(`Invalid eval_cases: ${evalCaseErrors.join("; ")}`);
2959
+ }
2960
+ const { id, ...rawBody } = spec;
2863
2961
  const body = canonicalizeSpecBaseModel(rawBody);
2864
2962
  let data;
2865
2963
  if (id) {
@@ -2881,7 +2979,7 @@ function registerPushCommand(parent) {
2881
2979
 
2882
2980
  // src/index.ts
2883
2981
  var program = new Command();
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(
2982
+ program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.17").option("-k, --api-key <key>", "API key (overrides stored key)").option(
2885
2983
  "-u, --base-url <url>",
2886
2984
  "API base URL (default: https://tunedtensor.com)"
2887
2985
  ).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {