@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 +17 -139
- package/dist/index.js +222 -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
|
@@ -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
|
-
|
|
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.
|
|
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) => {
|