@tuned-tensor/cli 0.4.13 → 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 +17 -139
- package/dist/index.js +217 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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)
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
# Models
|
|
99
|
-
tt models base
|
|
46
|
+
tt runs list
|
|
100
47
|
tt models list
|
|
101
|
-
tt models
|
|
102
|
-
tt
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
@@ -1091,6 +1091,10 @@ DEFAULT_MAX_TOKENS = int(os.environ.get("TT_MAX_TOKENS", "512"))
|
|
|
1091
1091
|
DEFAULT_TEMPERATURE = float(os.environ.get("TT_TEMPERATURE", "0.7"))
|
|
1092
1092
|
TRUST_REMOTE_CODE = os.environ.get("TT_TRUST_REMOTE_CODE", "false").lower() in {"1", "true", "yes", "y"}
|
|
1093
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"])
|
|
1094
1098
|
|
|
1095
1099
|
|
|
1096
1100
|
def choose_device(torch):
|
|
@@ -1179,6 +1183,50 @@ def normalize_messages(raw_messages):
|
|
|
1179
1183
|
return messages
|
|
1180
1184
|
|
|
1181
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
|
+
|
|
1182
1230
|
def render_prompt(messages):
|
|
1183
1231
|
chat_template = getattr(tokenizer, "chat_template", None)
|
|
1184
1232
|
if hasattr(tokenizer, "apply_chat_template") and chat_template:
|
|
@@ -1219,6 +1267,142 @@ def generate_completion(messages, max_tokens, temperature):
|
|
|
1219
1267
|
return content, prompt_tokens, int(completion_ids.shape[-1])
|
|
1220
1268
|
|
|
1221
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
|
+
|
|
1222
1406
|
class Handler(BaseHTTPRequestHandler):
|
|
1223
1407
|
server_version = "TunedTensorReferenceServer/0.1"
|
|
1224
1408
|
|
|
@@ -1267,7 +1451,23 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1267
1451
|
max_tokens = int(body.get("max_tokens") or DEFAULT_MAX_TOKENS)
|
|
1268
1452
|
temperature = float(body.get("temperature") if body.get("temperature") is not None else DEFAULT_TEMPERATURE)
|
|
1269
1453
|
messages = normalize_messages(raw_messages)
|
|
1270
|
-
|
|
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
|
|
1271
1471
|
|
|
1272
1472
|
self.send_json(200, {
|
|
1273
1473
|
"id": "chatcmpl-" + uuid.uuid4().hex,
|
|
@@ -1547,6 +1747,18 @@ function loadSystemPromptFromSpec(specPath) {
|
|
|
1547
1747
|
const spec = JSON.parse(readFileSync6(specPath, "utf8"));
|
|
1548
1748
|
return buildSystemPrompt(spec);
|
|
1549
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
|
+
}
|
|
1550
1762
|
function writeReferenceServerScript(cacheDir) {
|
|
1551
1763
|
const scriptDir = join2(cacheDir, "_server");
|
|
1552
1764
|
mkdirSync2(scriptDir, { recursive: true });
|
|
@@ -1726,7 +1938,7 @@ function registerModelsCommands(parent) {
|
|
|
1726
1938
|
printSuccess(`Serving runtime ready at ${venvDir}`);
|
|
1727
1939
|
console.log(`Use it with: tt models serve <model-id> --spec ${DEFAULT_SPEC_FILE}`);
|
|
1728
1940
|
});
|
|
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) => {
|
|
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) => {
|
|
1730
1942
|
const opts = parent.opts();
|
|
1731
1943
|
const cacheDir = resolve2(cmdOpts.cacheDir || defaultCacheDir());
|
|
1732
1944
|
mkdirSync2(cacheDir, { recursive: true });
|
|
@@ -1757,10 +1969,12 @@ function registerModelsCommands(parent) {
|
|
|
1757
1969
|
TT_PORT: String(cmdOpts.port),
|
|
1758
1970
|
TT_MAX_TOKENS: String(cmdOpts.maxTokens),
|
|
1759
1971
|
TT_TEMPERATURE: String(cmdOpts.temperature),
|
|
1972
|
+
TT_JSON_REPAIR_ATTEMPTS: String(cmdOpts.jsonRepairAttempts),
|
|
1760
1973
|
TT_TRUST_REMOTE_CODE: cmdOpts.trustRemoteCode ? "true" : "false",
|
|
1761
1974
|
TT_DEVICE: String(cmdOpts.device)
|
|
1762
1975
|
};
|
|
1763
1976
|
if (systemPrompt) env.TT_SYSTEM_PROMPT = systemPrompt;
|
|
1977
|
+
if (cmdOpts.jsonSchema) env.TT_JSON_SCHEMA = loadJsonSchemaForServe(cmdOpts.jsonSchema);
|
|
1764
1978
|
const args = [serverScript];
|
|
1765
1979
|
if (cmdOpts.printCommand) return printServeCommand(python, args, env);
|
|
1766
1980
|
if (!isJsonMode()) {
|
|
@@ -2091,7 +2305,7 @@ function registerPushCommand(parent) {
|
|
|
2091
2305
|
|
|
2092
2306
|
// src/index.ts
|
|
2093
2307
|
var program = new Command();
|
|
2094
|
-
program.name("tt").description("Tuned Tensor CLI \u2014 fine-tune and evaluate LLMs").version("0.4.
|
|
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(
|
|
2095
2309
|
"-u, --base-url <url>",
|
|
2096
2310
|
"API base URL (default: https://tunedtensor.com)"
|
|
2097
2311
|
).option("--json", "Output raw JSON").option("--no-color", "Disable colors").hook("preAction", (_thisCommand, actionCommand) => {
|