@tuned-tensor/local 0.1.7 → 0.1.9

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/CHANGELOG.md CHANGED
@@ -1,6 +1,54 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to Tuned Tensor Local will be documented in this file.
3
+ All notable changes to TT Local will be documented in this file.
4
+
5
+ ## 0.1.9 - 2026-07-03
6
+
7
+ ### Added
8
+
9
+ - Added a `tt-local label` command, porting the tuned-tensor-runs teacher
10
+ labeling job to the local runner. It reads unlabeled JSONL
11
+ (`{"input": "..."}` rows, optionally pre-labeled with `"output"`) or CSV
12
+ (`--input-column`) sources, labels pending rows with an OpenRouter teacher
13
+ model under the spec's system message, and writes a labeled
14
+ `{"input", "output"}` JSONL, a per-row review file, and a job report under
15
+ `<artifactRoot>/labeling/<job-id>/`. Supports `--dry-run`, `--output`,
16
+ `--model`, `--spec`, and `--system-prompt`.
17
+ - Rows are sanitized before leaving the machine: secret-like content (API
18
+ keys, private keys, connection strings, passwords) blocks the row; PII
19
+ (emails, phones, SSNs, card numbers) is redacted.
20
+ - Added a `labeling` runner-config block (`model`, `maxTokens`, `temperature`,
21
+ `concurrency`, `minIntervalMs`, `maxAttempts`, `maxRows`, `timeoutMs`); the
22
+ teacher model falls back to `llm.model`.
23
+
24
+ ### Changed
25
+
26
+ - `openRouterChat` now supports free-text responses (`responseFormat:
27
+ "text"`), `temperature`/`maxTokens` overrides, and returns prompt/completion
28
+ token usage; it throws a typed `OpenRouterHttpError` so callers can retry
29
+ retryable statuses. Judge behavior (JSON object output, temperature 0) is
30
+ unchanged by default.
31
+
32
+ ## 0.1.8 - 2026-07-02
33
+
34
+ ### Changed
35
+
36
+ - Renamed the user-facing project language to TT Local with the tagline
37
+ "Tuning tensors locally", while keeping the `tt-local` CLI, npm package, repo,
38
+ and filesystem identifiers unchanged.
39
+ - Reworked the README around a first-time user flow: install, safe dry run,
40
+ real training, config basics, evaluation behavior, artifacts, Spark notes,
41
+ and common commands.
42
+ - Simplified evaluation configuration by removing `evaluation.mode`. Inference
43
+ is now selected with `evaluation.inference.provider`, and scoring is selected
44
+ with `evaluation.scoring.mode`.
45
+ - Shared the uv/subprocess execution helper between training and evaluation so
46
+ logging, timeout, and stderr handling are consistent.
47
+
48
+ ### Added
49
+
50
+ - Added direct support for `evaluation.inference.provider: "command"` with
51
+ `baselineCommand` and `candidateCommand` for custom inference adapters.
4
52
 
5
53
  ## 0.1.7 - 2026-07-02
6
54
 
package/README.md CHANGED
@@ -1,62 +1,55 @@
1
- # Tuned Tensor Local
1
+ # TT Local
2
+
3
+ Tuning tensors locally.
2
4
 
3
5
  [![CI](https://github.com/tunedtensor/tuned-tensor-local/actions/workflows/ci.yml/badge.svg)](https://github.com/tunedtensor/tuned-tensor-local/actions/workflows/ci.yml)
4
6
  [![npm](https://img.shields.io/npm/v/@tuned-tensor/local)](https://www.npmjs.com/package/@tuned-tensor/local)
5
7
 
6
- Tuned Tensor Local is a standalone CLI for running Tuned Tensor-style fine-tuning
7
- jobs on a machine you control. It stores specs, datasets, model artifacts,
8
- progress, reports, and dashboard state on local disk.
8
+ TT Local is a standalone CLI for running Tuned Tensor-style
9
+ fine-tuning jobs on a machine you control. It stores specs, datasets, model
10
+ artifacts, progress events, reports, and dashboard state on local disk.
9
11
 
10
- It does not require the hosted Tuned Tensor CLI, cloud orchestration, Docker, or
11
- a managed database.
12
+ It does not require the hosted Tuned Tensor CLI, cloud orchestration, Docker,
13
+ or a managed database.
12
14
 
13
- ## What It Does
15
+ ## What Happens In A Run
14
16
 
15
- `tt-local` runs this workflow locally:
17
+ `tt-local` runs this loop:
16
18
 
17
19
  1. Read a behavior spec from `tunedtensor.json`.
18
- 2. Compile examples into a training dataset.
20
+ 2. Compile examples into a local training dataset.
19
21
  3. Evaluate the original Hugging Face base model.
20
22
  4. Fine-tune locally with uv, Transformers, and PEFT.
21
23
  5. Evaluate the fine-tuned Hugging Face/PEFT artifact.
22
- 6. Compare baseline vs tuned outputs and write a local report.
24
+ 6. Compare baseline vs tuned outputs and write a report.
23
25
 
24
- OpenRouter can be used as an LLM judge for scoring generated outputs. It does
25
- not generate the baseline or tuned responses.
26
+ OpenRouter can optionally score generated outputs as an LLM judge. It does not
27
+ generate the baseline or tuned responses.
26
28
 
27
- ## Install Locally
29
+ ## Install
28
30
 
29
- Recommended for development and Spark:
30
-
31
- ```bash
32
- git clone https://github.com/tunedtensor/tuned-tensor-local.git
33
- cd tuned-tensor-local
34
- npm install
35
- npm run build
36
- npm link
37
- ```
38
-
39
- You can install the published [@tuned-tensor/local npm package](https://www.npmjs.com/package/@tuned-tensor/local)
40
- globally:
31
+ Install the published CLI:
41
32
 
42
33
  ```bash
43
34
  npm install -g @tuned-tensor/local
35
+ tt-local info
44
36
  ```
45
37
 
46
- Check the CLI:
38
+ For local development or Spark:
47
39
 
48
40
  ```bash
41
+ git clone https://github.com/tunedtensor/tuned-tensor-local.git
42
+ cd tuned-tensor-local
43
+ npm install
44
+ npm run build
45
+ npm link
49
46
  tt-local info
50
47
  ```
51
48
 
52
- ## Set Up DGX Spark
53
-
54
- On the Spark host, confirm the GPU and uv are available:
49
+ Real training also needs `uv`:
55
50
 
56
51
  ```bash
57
- nvidia-smi
58
52
  uv --version
59
- uv run python --version
60
53
  ```
61
54
 
62
55
  If uv is missing:
@@ -65,29 +58,11 @@ If uv is missing:
65
58
  curl -LsSf https://astral.sh/uv/install.sh | sh
66
59
  ```
67
60
 
68
- Open a new shell, then rerun:
69
-
70
- ```bash
71
- uv --version
72
- ```
73
-
74
- Clone this repo on the Spark, install the Node dependencies, and build:
61
+ ## First Run: Safe Dry Run
75
62
 
76
- ```bash
77
- git clone https://github.com/tunedtensor/tuned-tensor-local.git
78
- cd tuned-tensor-local
79
- npm install
80
- npm run build
81
- npm link
82
- ```
83
-
84
- Run the doctor:
85
-
86
- ```bash
87
- tt-local doctor --config examples/local-runner.json
88
- ```
89
-
90
- ## Run Your First Job
63
+ The included example config is a dry run. It validates the end-to-end local
64
+ workflow and writes artifacts without downloading models or starting GPU
65
+ training.
91
66
 
92
67
  Create a local behavior spec:
93
68
 
@@ -110,16 +85,13 @@ Validate the spec and config:
110
85
  tt-local validate --config examples/local-runner.json
111
86
  ```
112
87
 
113
- Run the example dry workflow:
88
+ Run the dry workflow:
114
89
 
115
90
  ```bash
116
91
  tt-local run --config examples/local-runner.json
117
92
  ```
118
93
 
119
- The example config has `dryRun: true`, so it checks orchestration and artifact
120
- writing without loading models or starting GPU training.
121
-
122
- Inspect the run:
94
+ Inspect the result:
123
95
 
124
96
  ```bash
125
97
  tt-local runs list --config examples/local-runner.json
@@ -132,11 +104,11 @@ Start the local dashboard:
132
104
  tt-local serve --config examples/local-runner.json
133
105
  ```
134
106
 
135
- Then open the printed local URL in your browser.
107
+ Open the printed local URL in your browser.
136
108
 
137
- ## Run Real Training On Spark
109
+ ## Run Real Training
138
110
 
139
- Create a Spark config, for example `spark-runner.json`:
111
+ Create a config such as `local-runner.json` or `spark-runner.json`:
140
112
 
141
113
  ```json
142
114
  {
@@ -169,8 +141,109 @@ Create a Spark config, for example `spark-runner.json`:
169
141
  }
170
142
  ```
171
143
 
172
- For structured JSON tasks, use field-level scoring instead of whole-output
173
- exact matching:
144
+ If you use OpenRouter judging, set an API key:
145
+
146
+ ```bash
147
+ export OPENROUTER_API_KEY="your_openrouter_key"
148
+ ```
149
+
150
+ `tt-local` also loads a `.env` file from the working directory. Existing
151
+ environment variables win.
152
+
153
+ Run the job:
154
+
155
+ ```bash
156
+ tt-local doctor --config spark-runner.json
157
+ tt-local validate --config spark-runner.json
158
+ tt-local run --config spark-runner.json
159
+ ```
160
+
161
+ `tt-local run` prints live stage updates to stderr and writes the final JSON
162
+ summary to stdout. Use `--verbose` to stream Python subprocess output, or
163
+ `--quiet` for JSON-only automation.
164
+
165
+ Watch progress:
166
+
167
+ ```bash
168
+ tt-local runs watch <run-id> --config spark-runner.json
169
+ ```
170
+
171
+ View the final report:
172
+
173
+ ```bash
174
+ tt-local runs report <run-id> --config spark-runner.json
175
+ ```
176
+
177
+ Compare two completed runs:
178
+
179
+ ```bash
180
+ tt-local runs compare <run-id-a> <run-id-b> --config spark-runner.json
181
+ ```
182
+
183
+ The comparison aligns shared eval prompts, separates newly added eval examples,
184
+ and measures judge score noise for identical baseline outputs.
185
+
186
+ ## Configuration Basics
187
+
188
+ The main config fields are:
189
+
190
+ - `artifactRoot`: datasets, logs, model outputs, eval JSON, and run reports.
191
+ - `storeRoot`: run/spec/model metadata used by CLI listing and the dashboard.
192
+ - `dryRun`: when `true`, skips model loading and training.
193
+ - `training`: uv/Python training entrypoint and environment.
194
+ - `evaluation.inference`: how baseline and candidate outputs are generated.
195
+ - `evaluation.scoring`: how generated outputs are scored.
196
+ - `paths.modelCache`: Hugging Face cache location.
197
+
198
+ Supported inference providers:
199
+
200
+ - `"transformers"`: run the included local evaluator with Hugging Face/PEFT.
201
+ - `"command"`: call `baselineCommand` or `candidateCommand` for custom inference.
202
+ - `"none"`: skip inference and record empty local responses.
203
+
204
+ Supported scoring modes:
205
+
206
+ - `"exact_match"`: normalized exact match.
207
+ - `"llm_judge"`: OpenRouter judge, with exact-match fallback by default.
208
+ - `"json_fields"`: field-level comparison for structured JSON outputs.
209
+
210
+ When `evaluation.scoring.mode` is `"llm_judge"`, `fallback` controls judge
211
+ failures. `"exact_match"` keeps the run going and records fallback reasoning.
212
+ `"fail"` fails the run.
213
+
214
+ ## Evaluation Behavior
215
+
216
+ `evaluation.maxExamples` caps how many examples are scored. If it is unset,
217
+ the request hyperparameter `max_eval_examples` is used. An explicit
218
+ `maxExamples` in the config wins. When capped, the runner uses a deterministic
219
+ seeded sample rather than a prefix, so sorted or grouped eval files do not
220
+ bias results. The seed defaults to a hash of the run id and can be pinned with
221
+ `evaluation.sampleSeed`.
222
+
223
+ For spec-based runs without `dataset_prebuilt`, the runner holds out about 20%
224
+ of the spec examples for evaluation, with at least one training example and one
225
+ holdout when possible. With only one spec example, evaluation runs on that
226
+ example and reports `eval_split: "spec_examples"`.
227
+
228
+ For prebuilt datasets, `dataset_prebuilt.training` is always copied into the
229
+ run artifact as the training set. Evaluation uses `dataset_prebuilt.test` when
230
+ present, then `dataset_prebuilt.validation`. If neither is provided, a real
231
+ run fails because evaluation would otherwise measure the training split. Set
232
+ `evaluation.allowPrebuiltTrainingEval: true` to allow that explicitly.
233
+
234
+ Baseline evaluations are cached under `<storeRoot>/cache/baseline-evals/`.
235
+ Re-running an unchanged spec with the same model, eval examples, generation
236
+ settings, and scoring config reuses the previous baseline report. Set
237
+ `evaluation.baselineCache: false` to disable.
238
+
239
+ Training progress from Python metric lines and tqdm progress bars is recorded
240
+ as run events, so `tt-local runs watch` and per-run `progress.jsonl` show
241
+ epoch, loss, step, percentage, and ETA when the trainer emits them.
242
+
243
+ ## Structured JSON Scoring
244
+
245
+ For structured tasks, use field-level scoring instead of whole-output exact
246
+ matching:
174
247
 
175
248
  ```json
176
249
  {
@@ -183,143 +256,85 @@ exact matching:
183
256
  }
184
257
  ```
185
258
 
186
- The evaluation report still includes `exact_match_rate`, but `avg_score`,
187
- `pass_rate`, and `json_field_metrics` are based on the selected JSON fields.
188
259
  If `fields` is omitted, the evaluator scores every key in the expected JSON
189
- object for each example. A configured field that is missing from the expected
190
- JSON is always scored as incorrect (with a reasoning note), so misconfigured
191
- field lists cannot inflate scores.
192
-
193
- The judge receives the spec's compiled system message (system prompt,
194
- guidelines, and constraints) as task instructions and scores how well each
195
- output fulfills the task; the expected output is treated as a reference
196
- answer, not a fact checklist. Every eval report also includes `avg_token_f1`
197
- (and the comparison includes `token_f1_delta`), a deterministic token-overlap
198
- similarity against the reference outputs that is useful for free-text tasks
199
- such as summarization, where exact match is always 0 and judge scores can be
200
- noisy.
201
-
202
- Each eval example records how it was scored (`results[].scored_by`), and the
203
- report aggregates `judge_scored_count`, `fallback_scored_count`, and
204
- `judge_only_avg_score`. When a judge request fails transiently and the
205
- example falls back to exact match, `avg_score` mixes two metrics; use
206
- `judge_only_avg_score` (and the comparison's `judge_only_avg_score_delta`)
207
- when comparing judge quality. The comparison also classifies each regressed
208
- example's judge reasoning as `factual`, `omission`, `style`, `fallback`, or
209
- `other` and reports the counts in `regression_taxonomy`.
210
-
211
- Baseline evaluations are cached under `<storeRoot>/cache/baseline-evals/`:
212
- re-running a spec with an unchanged base model, eval examples, generation
213
- settings, and scoring config reuses the previous baseline report instead of
214
- re-running inference and judge calls. Cached reports carry `cached: true`.
215
- Set `evaluation.baselineCache: false` to disable. Reports containing
216
- fallback-scored examples are never cached, so a transient judge failure is
217
- not replayed into future runs.
218
-
219
- For thinking-mode base models (for example `Qwen/Qwen3.5-4B`, whose chat
220
- template opens a `<think>` block by default and can consume the entire
221
- `maxNewTokens` budget before any visible answer), forward template kwargs to
222
- both evaluation and training:
260
+ object. A configured field missing from the expected JSON is always scored as
261
+ incorrect, so a misconfigured field list cannot inflate scores.
223
262
 
224
- ```json
225
- {
226
- "evaluation": {
227
- "inference": { "chatTemplateKwargs": { "enable_thinking": false } }
228
- }
229
- }
230
- ```
263
+ Reports still include `exact_match_rate`, but `avg_score`, `pass_rate`, and
264
+ `json_field_metrics` are based on the selected JSON fields.
231
265
 
232
- and in the spec's hyperparameters:
233
-
234
- ```json
235
- { "chat_template_kwargs": { "enable_thinking": false } }
236
- ```
266
+ ## Labeling Data With A Teacher Model
237
267
 
238
- To compare two runs whose eval sets may differ (for example after raising
239
- `evaluation.maxExamples`), use:
268
+ `tt-local label` turns unlabeled inputs into training examples by asking a
269
+ stronger teacher model on OpenRouter to answer each row under your spec's
270
+ system message — the same instructions the fine-tuned model will see at
271
+ inference:
240
272
 
241
273
  ```bash
242
- tt-local runs compare <run-id-a> <run-id-b> --config spark-runner.json
274
+ export OPENROUTER_API_KEY=... # or put it in .env
275
+ tt-local label rows.jsonl --output labeled.jsonl
276
+ tt-local label rows.csv --input-column text --model anthropic/claude-fable-5
243
277
  ```
244
278
 
245
- It aligns the runs on their shared eval prompts, reports both runs' averages
246
- on that shared subset plus the new-example subset separately, and measures
247
- judge score noise on baseline outputs that are byte-identical across the two
248
- runs.
249
-
250
- With `"mode": "llm_judge"`, `scoring.fallback` controls what happens when the
251
- judge is unavailable, a judge request fails, or the judge returns malformed
252
- JSON. With `"fallback": "exact_match"` (the default), the affected example is
253
- scored by normalized exact match and its reasoning records the judge failure;
254
- the run does not fail. With `"fallback": "fail"`, judge errors fail the run.
255
-
256
- `evaluation.maxExamples` caps how many evaluation examples are scored. If it
257
- is unset, the request hyperparameter `max_eval_examples` is used instead;
258
- an explicit `maxExamples` in the config always wins. When the cap truncates
259
- the eval set, the runner draws a deterministic seeded random sample (not a
260
- prefix), so sorted or grouped eval files do not bias the subset. The seed
261
- defaults to a hash of the run id, can be pinned with `evaluation.sampleSeed`,
262
- and is recorded in each eval report as `eval_sample_seed`. Baseline and
263
- candidate evaluations always score the same sampled examples in the same
264
- order.
265
-
266
- If you want OpenRouter judging, set your key:
279
+ Sources are JSONL (`{"input": "..."}` per line; rows that already carry an
280
+ `"output"` skip the teacher) or CSV with a header row and `--input-column`.
281
+ The system message comes from `tunedtensor.json` (or `--spec <path>`);
282
+ pass `--system-prompt "..."` to override it. Use `--dry-run` to parse,
283
+ sanitize, and estimate without calling the teacher.
267
284
 
268
- ```bash
269
- export OPENROUTER_API_KEY="your_openrouter_key"
285
+ Because rows are sent to OpenRouter, each row is sanitized first:
286
+ secret-like content (API keys, private keys, connection strings) blocks the
287
+ row, and PII (emails, phone numbers, SSNs, card numbers) is redacted before
288
+ sending. The labeled `{"input", "output"}` JSONL, a full per-row
289
+ `rows.jsonl` (including failures for review), and a job report are written
290
+ under `<artifactRoot>/labeling/<job-id>/`.
291
+
292
+ Teacher behavior is configured in the runner config; the model falls back to
293
+ `llm.model`:
294
+
295
+ ```json
296
+ {
297
+ "labeling": {
298
+ "model": "anthropic/claude-fable-5",
299
+ "maxTokens": 2048,
300
+ "temperature": 0.2,
301
+ "concurrency": 4,
302
+ "maxAttempts": 3
303
+ }
304
+ }
270
305
  ```
271
306
 
272
- `tt-local` also loads a `.env` file from the working directory (existing
273
- environment variables win), so the key can live in the project's `.env`
274
- instead.
307
+ ## Thinking-Mode Models
275
308
 
276
- `validate` and `run` warn about hyperparameter keys the schema does not
277
- recognize (for example `per_device_train_batch_size` instead of
278
- `batch_size`); unknown keys are ignored rather than passed to training.
309
+ Some models, such as `Qwen/Qwen3.5-4B`, may open a hidden thinking block by
310
+ default and consume the token budget before producing a visible answer. Forward
311
+ template kwargs to both evaluation and training:
279
312
 
280
- Run the job:
313
+ ```json
314
+ {
315
+ "evaluation": {
316
+ "inference": {
317
+ "chatTemplateKwargs": { "enable_thinking": false }
318
+ }
319
+ }
320
+ }
321
+ ```
281
322
 
282
- ```bash
283
- tt-local run --config spark-runner.json
323
+ In `tunedtensor.json`, also set:
324
+
325
+ ```json
326
+ {
327
+ "hyperparameters": {
328
+ "chat_template_kwargs": { "enable_thinking": false }
329
+ }
330
+ }
284
331
  ```
285
332
 
286
- `tt-local run` prints live stage updates to stderr and writes the final JSON
287
- summary to stdout. Use `--verbose` to stream Python subprocess output, or
288
- `--quiet` for script-only JSON output.
289
-
290
- Training progress from Python metric lines and tqdm progress bars is also
291
- recorded as run events, so `tt-local runs watch` and per-run `progress.jsonl`
292
- show epoch, loss, step, percentage, and ETA updates when the trainer emits
293
- them.
294
-
295
- For spec-based runs (no `dataset_prebuilt`), the runner automatically holds
296
- out about 20% of the spec examples (at least 1 holdout and at least 1 training
297
- example) for evaluation, so baseline and candidate scores are not measured on
298
- the training data. The split is deterministic: it uses the per-run sample seed
299
- (a hash of the run id, or `evaluation.sampleSeed` when set), so re-running the
300
- same run id reproduces the same split and baseline/candidate always score the
301
- identical holdout. The training JSONL artifact contains only the training
302
- split, `run_metadata.training_example_count` records the training split size,
303
- and `eval_examples_total` records the holdout size. These runs report
304
- `eval_split: "spec_holdout"`. With only 1 spec example there is nothing to
305
- hold out, so evaluation runs on the training set and is reported as
306
- `eval_split: "spec_examples"` (training-set evaluation due to insufficient
307
- examples).
333
+ ## Multimodal Examples
308
334
 
309
- For prebuilt datasets, `dataset_prebuilt.training` is always copied into the
310
- run artifact as the training set. Evaluation uses `dataset_prebuilt.test` when
311
- present, then `dataset_prebuilt.validation`. If neither is provided, a real
312
- (non-dry) run fails with an error, because evaluating on the training split
313
- overstates improvement; set `evaluation.allowPrebuiltTrainingEval: true` to
314
- evaluate on the training file anyway. Dry runs are allowed to fall back to the
315
- training file without the override. Each eval report and the run's
316
- `run_metadata` record the split that was evaluated in `eval_split`
317
- (`spec_holdout`, `spec_examples`, `prebuilt_test`, `prebuilt_validation`, or
318
- `prebuilt_training`), so training-set evaluation is always visible.
319
-
320
- Multimodal runs use the same chat JSONL shape with structured user content.
321
- For `image_text_to_text` models such as `Qwen/Qwen3-VL-2B-Instruct`, image
322
- parts are loaded by the local SFT trainer and evaluator:
335
+ Multimodal runs use chat JSONL with structured user content. For
336
+ `image_text_to_text` models such as `Qwen/Qwen3-VL-2B-Instruct`, image parts
337
+ are loaded by the local trainer and evaluator:
323
338
 
324
339
  ```json
325
340
  {
@@ -340,34 +355,17 @@ parts are loaded by the local SFT trainer and evaluator:
340
355
  Image values in prebuilt JSONL may be absolute paths, paths relative to the
341
356
  JSONL file, `file://` URIs, HTTP(S) URLs, or `data:` URIs. Behavior spec
342
357
  examples can also use `input_assets` with `image`, `path`, `uri`, or
343
- `data_uri`; use absolute paths, `file://` URIs, URLs, or `data:` URIs there so
344
- the copied run artifact can still resolve the image.
345
-
346
- Watch progress:
347
-
348
- ```bash
349
- tt-local runs watch <run-id> --config spark-runner.json
350
- ```
351
-
352
- View the final report:
353
-
354
- ```bash
355
- tt-local runs report <run-id> --config spark-runner.json
356
- ```
358
+ `data_uri`.
357
359
 
358
360
  ## Where Files Go
359
361
 
360
- The runner writes two local roots:
361
-
362
- - `artifactRoot`: datasets, logs, model outputs, eval JSON, and `run-report.json`.
363
- - `storeRoot`: run/spec/model metadata used by CLI listing and the dashboard.
364
-
365
362
  If `storeRoot` is omitted, `tt-local` uses `TT_LOCAL_HOME` or
366
363
  `~/.tuned-tensor-local`.
367
364
 
368
365
  Per-run artifacts include:
369
366
 
370
- - `progress.jsonl`: stage changes and parsed training progress for the run.
367
+ - `progress.jsonl`: stage changes and parsed training progress.
368
+ - `baseline-eval.json` and `candidate-eval.json`: generated outputs and scores.
371
369
  - `baseline-eval.json.inference.log` and `candidate-eval.json.inference.log`:
372
370
  local inference subprocess output.
373
371
  - `training/training.log`: uv fine-tuning output.
@@ -377,18 +375,44 @@ Per-run artifacts include:
377
375
 
378
376
  ```bash
379
377
  tt-local init --name "Support Bot" --model Qwen/Qwen3.5-2B
378
+ tt-local doctor --config spark-runner.json
380
379
  tt-local validate --config spark-runner.json
381
380
  tt-local run --config spark-runner.json
382
381
  tt-local run --config spark-runner.json --verbose
382
+ tt-local label rows.jsonl --output labeled.jsonl
383
383
  tt-local runs list --config spark-runner.json
384
384
  tt-local runs get <run-id> --config spark-runner.json
385
385
  tt-local runs events <run-id> --config spark-runner.json
386
+ tt-local runs watch <run-id> --config spark-runner.json
386
387
  tt-local runs report <run-id> --config spark-runner.json
387
388
  tt-local runs compare <run-id-a> <run-id-b> --config spark-runner.json
388
389
  tt-local models list --config spark-runner.json
389
390
  tt-local serve --config spark-runner.json
390
391
  ```
391
392
 
393
+ ## DGX Spark Notes
394
+
395
+ On the Spark host, confirm the GPU and uv are available:
396
+
397
+ ```bash
398
+ nvidia-smi
399
+ uv --version
400
+ uv run python --version
401
+ ```
402
+
403
+ Then clone, install, build, and link the CLI on the Spark:
404
+
405
+ ```bash
406
+ git clone https://github.com/tunedtensor/tuned-tensor-local.git
407
+ cd tuned-tensor-local
408
+ npm install
409
+ npm run build
410
+ npm link
411
+ tt-local doctor --config examples/local-runner.json
412
+ ```
413
+
414
+ Spark-specific details live in [docs/spark.md](docs/spark.md).
415
+
392
416
  ## Development
393
417
 
394
418
  ```bash
@@ -404,12 +428,12 @@ secret.
404
428
 
405
429
  ## Notes
406
430
 
407
- - Real training uses the included SFT script in `training/sft-local/src/train.py`.
431
+ - Real training uses `training/sft-local/src/train.py`.
408
432
  - Real evaluation uses `training/sft-local/src/evaluate.py`.
409
- - The default evaluation path uses Hugging Face/PEFT artifacts directly; it does
410
- not convert models to GGUF.
433
+ - The default evaluation path uses Hugging Face/PEFT artifacts directly; it
434
+ does not convert models to GGUF.
435
+ - `validate` and `run` warn about unknown hyperparameter keys and ignore them.
411
436
  - Release notes are in [CHANGELOG.md](CHANGELOG.md).
412
- - Spark-specific details live in [docs/spark.md](docs/spark.md).
413
437
 
414
438
  ## License
415
439
 
@@ -580,6 +580,16 @@ export declare const runReportSchema: z.ZodObject<{
580
580
  created_at: z.ZodString;
581
581
  }, z.core.$strip>;
582
582
  export declare const commandSchema: z.ZodArray<z.ZodString>;
583
+ export declare const labelingConfigSchema: z.ZodObject<{
584
+ model: z.ZodOptional<z.ZodString>;
585
+ maxTokens: z.ZodDefault<z.ZodNumber>;
586
+ temperature: z.ZodDefault<z.ZodNumber>;
587
+ concurrency: z.ZodDefault<z.ZodNumber>;
588
+ minIntervalMs: z.ZodDefault<z.ZodNumber>;
589
+ maxAttempts: z.ZodDefault<z.ZodNumber>;
590
+ maxRows: z.ZodDefault<z.ZodNumber>;
591
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
592
+ }, z.core.$strip>;
583
593
  export declare const localRunnerConfigSchema: z.ZodObject<{
584
594
  storeRoot: z.ZodOptional<z.ZodString>;
585
595
  artifactRoot: z.ZodDefault<z.ZodString>;
@@ -599,15 +609,11 @@ export declare const localRunnerConfigSchema: z.ZodObject<{
599
609
  modelCache: z.ZodOptional<z.ZodString>;
600
610
  }, z.core.$strip>>;
601
611
  evaluation: z.ZodDefault<z.ZodObject<{
602
- mode: z.ZodDefault<z.ZodEnum<{
603
- command: "command";
604
- llm_judge: "llm_judge";
605
- heuristic: "heuristic";
606
- }>>;
607
612
  baselineCommand: z.ZodOptional<z.ZodArray<z.ZodString>>;
608
613
  candidateCommand: z.ZodOptional<z.ZodArray<z.ZodString>>;
609
614
  inference: z.ZodDefault<z.ZodObject<{
610
615
  provider: z.ZodDefault<z.ZodEnum<{
616
+ command: "command";
611
617
  none: "none";
612
618
  transformers: "transformers";
613
619
  }>>;
@@ -654,6 +660,16 @@ export declare const localRunnerConfigSchema: z.ZodObject<{
654
660
  appName: z.ZodOptional<z.ZodString>;
655
661
  siteUrl: z.ZodOptional<z.ZodString>;
656
662
  }, z.core.$strip>>;
663
+ labeling: z.ZodDefault<z.ZodObject<{
664
+ model: z.ZodOptional<z.ZodString>;
665
+ maxTokens: z.ZodDefault<z.ZodNumber>;
666
+ temperature: z.ZodDefault<z.ZodNumber>;
667
+ concurrency: z.ZodDefault<z.ZodNumber>;
668
+ minIntervalMs: z.ZodDefault<z.ZodNumber>;
669
+ maxAttempts: z.ZodDefault<z.ZodNumber>;
670
+ maxRows: z.ZodDefault<z.ZodNumber>;
671
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
672
+ }, z.core.$strip>>;
657
673
  }, z.core.$strip>;
658
674
  export type DocumentInputAsset = z.infer<typeof documentInputAssetSchema>;
659
675
  export type BehaviorSpecExample = z.infer<typeof behaviorSpecExampleSchema>;
@@ -667,4 +683,5 @@ export type EvalReport = z.infer<typeof evalReportSchema>;
667
683
  export type ComparisonReport = z.infer<typeof comparisonReportSchema>;
668
684
  export type TrainingReport = z.infer<typeof trainingReportSchema>;
669
685
  export type RunReport = z.infer<typeof runReportSchema>;
686
+ export type LocalLabelingConfig = z.infer<typeof labelingConfigSchema>;
670
687
  export type LocalRunnerConfig = z.infer<typeof localRunnerConfigSchema>;