@tuned-tensor/cli 0.4.12 → 0.4.14

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
@@ -1,6 +1,12 @@
1
1
  # tt - Tuned Tensor CLI
2
2
 
3
- `tt` is the command-line tool for [Tuned Tensor](https://www.tunedtensor.com), used to define behavior specs, validate them, and launch fine-tuning runs.
3
+ `tt` is the command-line tool for [Tuned Tensor](https://www.tunedtensor.com).
4
+ Use it to define behaviour specs, validate them, launch fine-tuning runs,
5
+ manage datasets, and download or serve trained models.
6
+
7
+ The main CLI documentation lives at
8
+ [tunedtensor.com/docs/cli](https://tunedtensor.com/docs/cli). This README is a
9
+ short install and development reference.
4
10
 
5
11
  ## Install
6
12
 
@@ -12,7 +18,7 @@ tt --version
12
18
  Run from source:
13
19
 
14
20
  ```bash
15
- git clone https://github.com/tuned-tensor/tuned-tensor-cli.git
21
+ git clone https://github.com/tunedtensor/tuned-tensor-cli.git
16
22
  cd tuned-tensor-cli
17
23
  npm install
18
24
  npm run build
@@ -21,155 +27,31 @@ npm link
21
27
 
22
28
  ## Quick Start
23
29
 
24
- 1) **Authenticate**
25
-
26
30
  ```bash
27
31
  tt auth login
28
- tt auth status
29
- ```
30
-
31
- 2) **Create a local spec**
32
-
33
- ```bash
34
- tt init
35
- # or:
36
- tt init --name "Customer Support Bot" --model "Qwen/Qwen3.5-2B"
37
- ```
38
-
39
- Supported spec base models are `Qwen/Qwen3.5-2B`, `Qwen/Qwen3.5-4B`, `google/gemma-4-E2B-it`, `google/gemma-4-E4B-it`, `meta-llama/Llama-3.2-3B-Instruct`, `microsoft/Phi-4-mini-instruct`, `ibm-granite/granite-3.3-2b-instruct`, and `bigcode/starcoder2-3b`.
32
+ tt init --name "Customer Support Bot" --model Qwen/Qwen3.5-2B
40
33
 
41
- You can print the same list from the CLI:
42
-
43
- ```bash
44
- tt models base
45
- tt models base --json
46
- ```
47
-
48
- 3) **Validate your spec**
49
-
50
- ```bash
34
+ # Edit tunedtensor.json, then:
51
35
  tt eval
52
- ```
53
-
54
- 4) **Push your spec**
55
-
56
- ```bash
57
36
  tt push
58
- ```
59
-
60
- 5) **Start and watch a run**
61
-
62
- ```bash
63
37
  tt runs start <spec-id>
64
- tt runs start <spec-id> --dataset <dataset-id-or-prefix> --train-ratio 0.8 --validation-ratio 0.1 --test-ratio 0.1
65
- tt runs start <spec-id> --no-llm-judge
66
38
  tt runs watch <run-id>
67
39
  ```
68
40
 
69
- Tip: use `tt specs list`, `tt datasets list`, `tt runs list`, and `tt models list` to find IDs. Spec, run, dataset, and model commands accept full UUIDs or unambiguous ID prefixes.
70
-
71
- ## Typical Workflows
41
+ Useful discovery commands:
72
42
 
73
43
  ```bash
74
- # Account
75
- tt auth status
76
- tt balance
77
- tt topup --amount 25
78
-
79
- # Specs
80
44
  tt specs list
81
- tt specs get <spec-id>
82
- tt specs create --file spec.json
83
- tt specs update <spec-id> --file updates.json
84
-
85
- # Runs
86
- tt runs list --spec <spec-id>
87
- tt runs get <run-id>
88
- tt runs start <spec-id> --epochs 5 --lr 0.0001 --batch-size 8
89
- tt runs start <spec-id> --dataset <dataset-id-or-prefix> --train-ratio 0.8 --validation-ratio 0.1 --test-ratio 0.1
90
- tt runs start <spec-id> --no-llm-judge
91
- tt runs cancel <run-id>
92
-
93
- # Datasets
94
- tt datasets upload data.jsonl --name "Support Training Set"
95
45
  tt datasets list
96
- tt datasets get <dataset-id>
97
-
98
- # Models
99
- tt models base
46
+ tt runs list
100
47
  tt models list
101
- tt models get <model-id>
102
- tt models download <model-id> --output model.tar.gz
103
- tt models setup-runtime
104
- tt models serve <model-id> --spec tunedtensor.json
105
- tt models serve ./model --port 8000
106
- ```
107
-
108
- Use `--dataset <dataset-id-or-prefix>` with `tt runs start` to train from an uploaded dataset instead of inline spec examples. Add `--train-ratio`, `--validation-ratio`, and `--test-ratio` to override the default 80/10/10 split.
109
-
110
- Use `--max-eval-examples <n>` and `--max-test-eval-examples <n>` with `tt runs start` to cap primary and secondary test evaluation passes for larger datasets; the runs backend still clamps values to its configured ceiling.
111
-
112
- Use `--no-llm-judge` with `tt runs start` to opt out of LLM judging for a new run.
113
-
114
- `tt models download` downloads models that have a Tuned Tensor-hosted artifact. In interactive terminals it shows download progress, transfer rate, and ETA; `--json` output remains machine-readable. Hosted models can still be used for inference through their model ID, but may not expose downloadable weights.
115
-
116
- `tt models setup-runtime` installs an isolated local Python runtime for reference serving. It chooses Python 3.10-3.12, creates a managed venv in the Tuned Tensor cache, and installs `torch`, `transformers`, `accelerate`, and `safetensors`. This can take a few minutes the first time.
117
-
118
- `tt models serve` starts a local OpenAI-compatible reference server for a model ID, an extracted model directory, or a `.tar.gz` artifact. It auto-detects `tunedtensor.json` in the current directory or model directory and applies the compiled behavior spec as the default system prompt, so inference matches the prompt contract used during training. Use `--spec <path>` to point at a specific spec, or `--no-spec-prompt` when you intentionally want raw model behavior. If no compatible Python serving runtime is available, run `tt models setup-runtime` first.
119
-
120
- ```bash
121
- tt models setup-runtime
122
- tt models serve <model-id> --spec tunedtensor.json
123
- tt models serve <model-id> --spec tunedtensor.json --device mps # Apple Silicon
124
- tt models serve <model-id> --spec tunedtensor.json --device cuda # NVIDIA GPU
125
-
126
- curl http://127.0.0.1:8000/v1/chat/completions \
127
- -H 'content-type: application/json' \
128
- -d '{"messages":[{"role":"user","content":"Hello"}]}'
129
- ```
130
-
131
- ## Billing & Credits
132
-
133
- Tuned Tensor uses prepaid credits. New accounts start at a zero balance, so top up before starting your first fine-tuning run; you only pay for successful runs.
134
-
135
- ```bash
136
- tt balance # show credit balance and recent transactions
137
- tt topup --amount 25 # opens Stripe Checkout in your browser
138
- tt topup --amount 25 --no-open # print the URL instead
139
- ```
140
-
141
- `tt balance` shows your current credit balance. If a run is rejected with
142
- `402 insufficient_credits`, top up and retry.
143
-
144
- ## Spec Validation
145
-
146
- `tt eval` validates your local `tunedtensor.json`. It checks required fields, confirms examples are present, warns when guidelines are missing, and checks simple constraints against example outputs. It does not call a model or the Playground API.
147
-
148
- ## Global Flags
149
-
150
- - `-k, --api-key <key>`: override stored API key
151
- - `-u, --base-url <url>`: override API base URL
152
- - `--json`: machine-readable output
153
- - `--no-color`: disable ANSI colors
154
- - `-h, --help`: command help
155
-
156
- Examples:
157
-
158
- ```bash
159
- tt specs list --json
160
- tt runs get <run-id> --json
161
- tt runs start --help
48
+ tt models base
49
+ tt balance
162
50
  ```
163
51
 
164
- ## Configuration
165
-
166
- Credentials are stored in `~/.config/tuned-tensor/config.json` (respects `XDG_CONFIG_HOME`).
167
-
168
- API key precedence:
169
-
170
- 1. `--api-key`
171
- 2. `TUNED_TENSOR_API_KEY`
172
- 3. stored config
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).
173
55
 
174
56
  ## Development
175
57
 
@@ -181,10 +63,6 @@ npm run typecheck
181
63
  npm test
182
64
  ```
183
65
 
184
- ## Troubleshooting
185
-
186
- If the API rejects a spec with a generic server error, check that `base_model` is one of the supported spec base models listed above.
187
-
188
66
  ## License
189
67
 
190
68
  MIT
package/dist/index.js CHANGED
@@ -615,8 +615,13 @@ function printDiagnostics(diagnostics) {
615
615
  ["Stage", diagnostics.stage_label ?? diagnostics.stage ?? void 0],
616
616
  ["Summary", diagnostics.summary],
617
617
  ["Training", diagnostics.training.state],
618
+ ["Phase", diagnostics.training.phase ?? void 0],
618
619
  ["Epoch", formatEpochProgress(diagnostics)],
619
620
  ["Latest Loss", curve.latest_loss == null ? void 0 : curve.latest_loss.toFixed(4)],
621
+ [
622
+ "Token Accuracy",
623
+ curve.latest_token_accuracy == null ? void 0 : (curve.latest_token_accuracy * 100).toFixed(1) + "%"
624
+ ],
620
625
  ["Pace", perFive ? `${perFive} epoch / 5m` : void 0],
621
626
  ["ETA", formatMinutes(curve.estimated_minutes_remaining)],
622
627
  ["Latest Update", formatDate(diagnostics.training.last_updated_at)]
@@ -1086,6 +1091,10 @@ DEFAULT_MAX_TOKENS = int(os.environ.get("TT_MAX_TOKENS", "512"))
1086
1091
  DEFAULT_TEMPERATURE = float(os.environ.get("TT_TEMPERATURE", "0.7"))
1087
1092
  TRUST_REMOTE_CODE = os.environ.get("TT_TRUST_REMOTE_CODE", "false").lower() in {"1", "true", "yes", "y"}
1088
1093
  REQUESTED_DEVICE = os.environ.get("TT_DEVICE", "auto").lower()
1094
+ JSON_REPAIR_ATTEMPTS = int(os.environ.get("TT_JSON_REPAIR_ATTEMPTS", "1"))
1095
+ DEFAULT_JSON_SCHEMA = None
1096
+ if os.environ.get("TT_JSON_SCHEMA"):
1097
+ DEFAULT_JSON_SCHEMA = json.loads(os.environ["TT_JSON_SCHEMA"])
1089
1098
 
1090
1099
 
1091
1100
  def choose_device(torch):
@@ -1174,6 +1183,50 @@ def normalize_messages(raw_messages):
1174
1183
  return messages
1175
1184
 
1176
1185
 
1186
+ def response_json_contract(body):
1187
+ response_format = body.get("response_format")
1188
+ schema = DEFAULT_JSON_SCHEMA
1189
+ mode = "json_schema" if schema else "text"
1190
+
1191
+ if isinstance(response_format, dict):
1192
+ format_type = response_format.get("type")
1193
+ if format_type == "json_object":
1194
+ mode = "json_object"
1195
+ elif format_type == "json_schema":
1196
+ mode = "json_schema"
1197
+ json_schema = response_format.get("json_schema")
1198
+ if isinstance(json_schema, dict):
1199
+ schema = json_schema.get("schema") if isinstance(json_schema.get("schema"), dict) else json_schema
1200
+
1201
+ return mode, schema
1202
+
1203
+
1204
+ def with_json_contract(messages, mode, schema):
1205
+ if mode == "text":
1206
+ return messages
1207
+
1208
+ if schema:
1209
+ contract = (
1210
+ "Return only a valid JSON object matching this JSON Schema. "
1211
+ "Do not include markdown, code fences, comments, or prose outside the JSON object.\n"
1212
+ + json.dumps(schema, ensure_ascii=False, separators=(",", ":"))
1213
+ )
1214
+ else:
1215
+ contract = (
1216
+ "Return only a valid JSON object. Do not include markdown, code fences, "
1217
+ "comments, or prose outside the JSON object."
1218
+ )
1219
+
1220
+ contracted = [dict(message) for message in messages]
1221
+ for message in contracted:
1222
+ if message["role"] == "system":
1223
+ message["content"] = (message["content"].rstrip() + "\n\n" + contract).strip()
1224
+ return contracted
1225
+
1226
+ contracted.insert(0, {"role": "system", "content": contract})
1227
+ return contracted
1228
+
1229
+
1177
1230
  def render_prompt(messages):
1178
1231
  chat_template = getattr(tokenizer, "chat_template", None)
1179
1232
  if hasattr(tokenizer, "apply_chat_template") and chat_template:
@@ -1214,6 +1267,142 @@ def generate_completion(messages, max_tokens, temperature):
1214
1267
  return content, prompt_tokens, int(completion_ids.shape[-1])
1215
1268
 
1216
1269
 
1270
+ def extract_json_value(content):
1271
+ decoder = json.JSONDecoder()
1272
+ text = content.strip()
1273
+ try:
1274
+ return json.loads(text), None
1275
+ except Exception:
1276
+ pass
1277
+
1278
+ for index, char in enumerate(text):
1279
+ if char not in "{[":
1280
+ continue
1281
+ try:
1282
+ value, end = decoder.raw_decode(text[index:])
1283
+ trailing = text[index + end :].strip()
1284
+ if trailing:
1285
+ continue
1286
+ return value, None
1287
+ except Exception:
1288
+ continue
1289
+ return None, "response is not valid JSON"
1290
+
1291
+
1292
+ def json_type_matches(value, expected):
1293
+ if expected == "object":
1294
+ return isinstance(value, dict)
1295
+ if expected == "array":
1296
+ return isinstance(value, list)
1297
+ if expected == "string":
1298
+ return isinstance(value, str)
1299
+ if expected == "boolean":
1300
+ return isinstance(value, bool)
1301
+ if expected == "integer":
1302
+ return isinstance(value, int) and not isinstance(value, bool)
1303
+ if expected == "number":
1304
+ return (isinstance(value, int) or isinstance(value, float)) and not isinstance(value, bool)
1305
+ if expected == "null":
1306
+ return value is None
1307
+ return True
1308
+
1309
+
1310
+ def validate_json_schema(value, schema, path="$"):
1311
+ if not isinstance(schema, dict):
1312
+ return []
1313
+
1314
+ errors = []
1315
+ expected_type = schema.get("type")
1316
+ if isinstance(expected_type, list):
1317
+ if not any(json_type_matches(value, item) for item in expected_type):
1318
+ errors.append(f"{path} has wrong type")
1319
+ return errors
1320
+ elif isinstance(expected_type, str) and not json_type_matches(value, expected_type):
1321
+ errors.append(f"{path} must be {expected_type}")
1322
+ return errors
1323
+
1324
+ if "enum" in schema and value not in schema["enum"]:
1325
+ errors.append(f"{path} must be one of {schema['enum']}")
1326
+
1327
+ if isinstance(value, dict):
1328
+ required = schema.get("required") if isinstance(schema.get("required"), list) else []
1329
+ for key in required:
1330
+ if key not in value:
1331
+ errors.append(f"{path}.{key} is required")
1332
+
1333
+ properties = schema.get("properties") if isinstance(schema.get("properties"), dict) else {}
1334
+ for key, child_schema in properties.items():
1335
+ if key in value:
1336
+ errors.extend(validate_json_schema(value[key], child_schema, f"{path}.{key}"))
1337
+
1338
+ if schema.get("additionalProperties") is False:
1339
+ extras = sorted(set(value.keys()) - set(properties.keys()))
1340
+ for key in extras:
1341
+ errors.append(f"{path}.{key} is not allowed")
1342
+
1343
+ if isinstance(value, list) and isinstance(schema.get("items"), dict):
1344
+ for index, item in enumerate(value):
1345
+ errors.extend(validate_json_schema(item, schema["items"], f"{path}[{index}]"))
1346
+
1347
+ return errors
1348
+
1349
+
1350
+ def normalize_json_content(content, mode, schema):
1351
+ if mode == "text":
1352
+ return content, []
1353
+
1354
+ value, error = extract_json_value(content)
1355
+ if error:
1356
+ return content, [error]
1357
+ if mode in {"json_object", "json_schema"} and not isinstance(value, dict):
1358
+ return content, ["response must be a JSON object"]
1359
+
1360
+ errors = validate_json_schema(value, schema) if schema else []
1361
+ if errors:
1362
+ return content, errors
1363
+
1364
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":")), []
1365
+
1366
+
1367
+ def generate_validated_completion(messages, max_tokens, temperature, mode, schema):
1368
+ contracted_messages = with_json_contract(messages, mode, schema)
1369
+ attempts = max(0, JSON_REPAIR_ATTEMPTS) + 1
1370
+ last_content = ""
1371
+ last_prompt_tokens = 0
1372
+ last_completion_tokens = 0
1373
+ last_errors = []
1374
+
1375
+ for attempt in range(attempts):
1376
+ content, prompt_tokens, completion_tokens = generate_completion(
1377
+ contracted_messages,
1378
+ max_tokens,
1379
+ temperature,
1380
+ )
1381
+ normalized, errors = normalize_json_content(content, mode, schema)
1382
+ if not errors:
1383
+ return normalized, prompt_tokens, completion_tokens, []
1384
+
1385
+ last_content = content
1386
+ last_prompt_tokens = prompt_tokens
1387
+ last_completion_tokens = completion_tokens
1388
+ last_errors = errors
1389
+
1390
+ if attempt < attempts - 1:
1391
+ contracted_messages = contracted_messages + [
1392
+ {"role": "assistant", "content": content},
1393
+ {
1394
+ "role": "user",
1395
+ "content": (
1396
+ "The previous response was invalid: "
1397
+ + "; ".join(errors[:5])
1398
+ + ". Return only a corrected JSON object."
1399
+ ),
1400
+ },
1401
+ ]
1402
+
1403
+ return last_content, last_prompt_tokens, last_completion_tokens, last_errors
1404
+
1405
+
1217
1406
  class Handler(BaseHTTPRequestHandler):
1218
1407
  server_version = "TunedTensorReferenceServer/0.1"
1219
1408
 
@@ -1262,7 +1451,23 @@ class Handler(BaseHTTPRequestHandler):
1262
1451
  max_tokens = int(body.get("max_tokens") or DEFAULT_MAX_TOKENS)
1263
1452
  temperature = float(body.get("temperature") if body.get("temperature") is not None else DEFAULT_TEMPERATURE)
1264
1453
  messages = normalize_messages(raw_messages)
1265
- content, prompt_tokens, completion_tokens = generate_completion(messages, max_tokens, temperature)
1454
+ json_mode, json_schema = response_json_contract(body)
1455
+ content, prompt_tokens, completion_tokens, validation_errors = generate_validated_completion(
1456
+ messages,
1457
+ max_tokens,
1458
+ temperature,
1459
+ json_mode,
1460
+ json_schema,
1461
+ )
1462
+ if validation_errors:
1463
+ self.send_json(422, {
1464
+ "error": {
1465
+ "message": "Model did not produce valid JSON for the requested response_format.",
1466
+ "details": validation_errors,
1467
+ "content": content,
1468
+ }
1469
+ })
1470
+ return
1266
1471
 
1267
1472
  self.send_json(200, {
1268
1473
  "id": "chatcmpl-" + uuid.uuid4().hex,
@@ -1542,6 +1747,18 @@ function loadSystemPromptFromSpec(specPath) {
1542
1747
  const spec = JSON.parse(readFileSync6(specPath, "utf8"));
1543
1748
  return buildSystemPrompt(spec);
1544
1749
  }
1750
+ function loadJsonSchemaForServe(schemaPath) {
1751
+ const resolved = resolve2(schemaPath);
1752
+ if (!existsSync4(resolved) || !statSync3(resolved).isFile()) {
1753
+ throw new Error(`JSON schema file not found: ${schemaPath}`);
1754
+ }
1755
+ try {
1756
+ const schema = JSON.parse(readFileSync6(resolved, "utf8"));
1757
+ return JSON.stringify(schema);
1758
+ } catch (err) {
1759
+ throw new Error(`JSON schema file is not valid JSON: ${schemaPath}`);
1760
+ }
1761
+ }
1545
1762
  function writeReferenceServerScript(cacheDir) {
1546
1763
  const scriptDir = join2(cacheDir, "_server");
1547
1764
  mkdirSync2(scriptDir, { recursive: true });
@@ -1721,7 +1938,7 @@ function registerModelsCommands(parent) {
1721
1938
  printSuccess(`Serving runtime ready at ${venvDir}`);
1722
1939
  console.log(`Use it with: tt models serve <model-id> --spec ${DEFAULT_SPEC_FILE}`);
1723
1940
  });
1724
- 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) => {
1941
+ models.command("serve").description("Serve a downloaded model with an OpenAI-compatible local API").argument("<target>", "Model ID/prefix, model directory, or .tar.gz artifact").option("--spec <path>", "Behavior spec JSON to apply as the default system prompt").option("--no-spec-prompt", "Do not auto-apply a behavior spec system prompt").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "8000").option("--python <path>", "Python executable").option("--cache-dir <path>", "Cache directory for downloaded/extracted models").option("--device <device>", "Inference device: auto, cpu, cuda, or mps", "auto").option("--force-download", "Re-download and re-extract model artifacts").option("--max-tokens <n>", "Default max completion tokens", "512").option("--temperature <n>", "Default sampling temperature", "0.7").option("--json-schema <path>", "JSON Schema file to enforce by default for chat completions").option("--json-repair-attempts <n>", "Number of JSON/schema repair retries", "1").option("--trust-remote-code", "Pass trust_remote_code=True to transformers").option("--print-command", "Print the underlying Python command without starting it").action(async (target, cmdOpts) => {
1725
1942
  const opts = parent.opts();
1726
1943
  const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
1727
1944
  mkdirSync2(cacheDir, { recursive: true });
@@ -1752,10 +1969,12 @@ function registerModelsCommands(parent) {
1752
1969
  TT_PORT: String(cmdOpts.port),
1753
1970
  TT_MAX_TOKENS: String(cmdOpts.maxTokens),
1754
1971
  TT_TEMPERATURE: String(cmdOpts.temperature),
1972
+ TT_JSON_REPAIR_ATTEMPTS: String(cmdOpts.jsonRepairAttempts),
1755
1973
  TT_TRUST_REMOTE_CODE: cmdOpts.trustRemoteCode ? "true" : "false",
1756
1974
  TT_DEVICE: String(cmdOpts.device)
1757
1975
  };
1758
1976
  if (systemPrompt) env.TT_SYSTEM_PROMPT = systemPrompt;
1977
+ if (cmdOpts.jsonSchema) env.TT_JSON_SCHEMA = loadJsonSchemaForServe(cmdOpts.jsonSchema);
1759
1978
  const args = [serverScript];
1760
1979
  if (cmdOpts.printCommand) return printServeCommand(python, args, env);
1761
1980
  if (!isJsonMode()) {
@@ -2086,7 +2305,7 @@ function registerPushCommand(parent) {
2086
2305
 
2087
2306
  // src/index.ts
2088
2307
  var program = new Command();
2089
- program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.12").option("-k, --api-key <key>", "API key (overrides stored key)").option(
2308
+ program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.14").option("-k, --api-key <key>", "API key (overrides stored key)").option(
2090
2309
  "-u, --base-url <url>",
2091
2310
  "API base URL (default: https://tunedtensor.com)"
2092
2311
  ).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {