smithtune turns LangSmith trajectories into a fine-tuned model:
dataset pull → (dataset triage) → dataset push build a LangSmith dataset
prepare → plan → train --evaluate train and compare base vs tuned
deploy → undeploy optional endpoint
Installation and credentials live in the
README. This
skill covers how to operate the flow: which command to run, in what order, what
to check after each one, and when you are done. Run smithtune <command> --help
before an unfamiliar command.
Rules that apply to every step
- Parse the JSON. Every command prints a JSON result on stdout and progress
on stderr. Keep the IDs and paths it returns, and follow its
next_commandwhen present instead of guessing the next step. - Preview before paying.
triage,resume,push,train,evaluate,deploy, andundeploydo nothing billable or remote-changing without--confirm. Run each one without--confirmfirst, show the user the preview, and add--confirmonly when the user has authorized that specific operation. Authorization for one step does not cover the next. - One directory per stage, reused throughout. The same dataset directory for
pull/triage/push/resume; the same--data-dirforprepare/plan/train/evaluate; a new or empty--run-dirper training run. Settings are frozen into these directories once work starts. To change source, filter, time window, limits, review mode, or rubric, use a new directory. - Same provider throughout. One
--provider(fireworksorbaseten) frompreparethroughdeploy. - Never guess IDs, fields, or thresholds. Ask for workspace, project, and dataset IDs. Build filters only from fields you have seen in the project.
- Never print credential values. Check presence with
smithtune doctor.
0. Before anything
smithtune doctor
smithtune models list --provider "$provider"
Check:
doctorshows the keys the user's path needs asset(presence only, not validity);credentials_required_forsays what each key is for:
| Path | Keys |
|---|---|
| Baseten | LANGSMITH_API_KEY, BASETEN_API_KEY (Model API access covers the default council and evaluation judge) |
| Fireworks, with a Baseten key | LANGSMITH_API_KEY, FIREWORKS_API_KEY, BASETEN_API_KEY for the default judges |
| Fireworks only | LANGSMITH_API_KEY, FIREWORKS_API_KEY; then pass --judges deepseek-v4.1-flash,glm-5.3-flash to triage and set judge_model=accounts/fireworks/models/deepseek-v4p1-flash |
- The requested model is in
models list. - Data rights are acknowledged. If not, the user must run
smithtune acknowledge-data-rightsin an interactive terminal; you cannot do it for them and--confirmdoes not replace it.
Set the values used below once, and reuse them in every command:
provider=baseten # or fireworks; --provider defaults to fireworks, so always pass it
model=qwen3p8-27b # from `smithtune models list`
workspace_id='<workspace-id>'
project_id='<project-id>'
dataset_dir=./data/datasets/my-sft
data_dir=./data/my-sft
run_dir=./runs/my-sft
judge_model=baseten/zai-org/GLM-5.3-Flash # default; see the table above for Fireworks only
start_time=2026-09-01T00:00:00Z # ISO 8601 with timezone
end_time=2026-09-22T00:00:00Z
Pick the starting point:
| The user has | Start at |
|---|---|
| Traces in a tracing project | Step 1 |
| A LangSmith trajectory dataset | Step 5 (prepare) |
A prepared --data-dir |
Step 6 (plan) |
A finished --run-dir |
Step 8 (evaluate) or step 9 (deploy) |
Continue from existing directories when they match the task. For an existing
LangSmith dataset that smithtune did not create, prepare recovers per-turn
tool data from the source traces only when the example's messages match the
source trajectory exactly; otherwise it rejects those examples.
1. Choose the source and write the filter
The filter decides what the model learns from. Do this carefully and with the
user; pull freezes it into the directory.
1a. Identify the project. Get the workspace ID and project ID from the user. If they only know the name, list projects:
langsmith project list --workspace "$workspace_id" --format json
1b. Look at real root runs. Filters match root runs (traces), so inspect roots, not child LLM calls:
langsmith trace list --workspace "$workspace_id" --project-id "$project_id" \
--since 2026-09-01T00:00:00Z --limit 20 --full --format json
From the output, write down what actually exists:
- root
namevalues (which agent or graph produced the trace); tagsandcustom_metadatakeys and typical values (environment, version, customer tier, etc.);feedback_statskeys and their score ranges (e.g.correctness0–1,user_thumbs0/1) and how many roots have each key;- how many roots errored.
Inspect relevant online evaluators when they exist. Before treating their feedback as a trusted selection signal, inspect:
- the evaluation criteria, output schema, emitted feedback key, and score direction—what constitutes a pass or failure;
- what is evaluated: an input, output, individual run or action, tool behavior, complete trace, or conversation/thread;
- configured sampling and filters, and observed feedback coverage within the requested time window; distinguish root feedback from child-run feedback;
- whether the signal measures correctness or another task requirement, or infers quality indirectly, such as from apparent user satisfaction.
Check a few high/low or pass/fail scored examples against the criteria. If a signal's meaning cannot be established, or its definition conflicts with recorded scores, explain the uncertainty before recommending it as a selection condition. Assess whether the signal covers the behavior the model will learn; a positive score on one output or action does not validate the whole training trajectory.
Present the relevant signals, their meaning and coverage, and any uncertainty before recommending selection conditions or council review. These checks apply to both conversational projects and mechanical workflows; use the project's actual task requirements rather than assuming dialogue or user reactions.
1c. Agree with the user what "good training data" means, then map it to those fields. Ask:
- Which agent, workflow, or task (root name) should the model imitate?
- Is there feedback that means the run was good? Which keys and values or thresholds indicate good examples? What fraction of traces have each signal?
- If combining feedback conditions, must all or any positive conditions match? Which failure signals exclude candidates, and how should missing scores be handled? Treat missing scores as unknown until that choice is agreed.
- Any environment, version, or tag restrictions (e.g. production only, after a prompt change)?
- What time window?
Do not invent feedback keys, thresholds, or what a missing score means. If there is no trustworthy quality signal, say so; that is the case for council review in step 3.
1d. Write the filter in LangSmith filter syntax. Common building blocks:
| Intent | Filter |
|---|---|
| One agent | eq(name, "support-agent") |
| Has a tag | has(tags, "production") |
| Metadata value | and(eq(metadata_key, "env"), eq(metadata_value, "prod")) |
| Good feedback | and(eq(feedback_key, "correctness"), gte(feedback_score, 0.9)) |
| One known root | eq(id, "<root-run-id>") (time bounds must include it) |
| Combine | and(eq(name, "support-agent"), has(tags, "production")) |
Feedback conditions may combine several keys: require multiple positive signals, accept alternative positive signals, or exclude known failures. Keep each feedback key paired with its own value or threshold, then test the combined filter against actual returned scores. Measure how the combination changes coverage; multiple matching roots in one thread may still produce only one training trajectory.
1e. Test the filter before pulling. langsmith trace list --filter takes
the same syntax and costs nothing. Use the same window you will pull:
langsmith trace list --workspace "$workspace_id" --project-id "$project_id" \
--filter "$filter" --since "$start_time" --before "$end_time" \
--limit 50 --full --format json
Check that:
- the matches are the agent and behavior the user wants (open a few);
- there are enough of them for the target (the smithtune default target is 100);
- nothing obviously wrong slipped in (errors, test traffic, other agents).
Adjust and re-test until the user agrees. Only then pull.
Know what the filter does not limit: each matching root pulls in its whole thread, including earlier turns and turns outside the filter and time window. The training example is the full trajectory.
2. Pull
Ask the user whether to review with a council before pulling. The choice is fixed for the directory, so settle it now. Explain both options:
| No council review | Council review | |
|---|---|---|
| Use when | The filter already selects on a trusted quality signal (validated feedback score, human labels) | There is no trustworthy quality signal, or the user wants a second check |
| Pull flag | --no-triage |
none (this is the CLI default) |
| What the target counts | Structurally usable trajectories | Council-approved trajectories |
| Extra keys | None | Judge keys (BASETEN_API_KEY for the default council) |
| Extra work | None; go straight to push | Write a rubric with the user (step 3) |
| Cost | No model calls | One judge call per trajectory per council member |
Recommend one based on what you found in step 1: if the user's quality signal is real and well covered, suggest no review; if not, suggest council review. An agent-name filter or "no errors" alone is not a quality signal.
Existing quality proxies can also prefilter candidates before council review, even when they are insufficient to justify skipping review. Explain this option when relevant, including how unscored candidates would be handled. The source selection can therefore use trusted feedback without council review, feedback prefiltering plus council review, or council review without a feedback prefilter. Let the user decide, and record both the selection conditions and the review choice.
Because omitting the flag means council review, always pass --no-triage
explicitly when the user chose no review.
smithtune dataset pull "$dataset_dir" \
--workspace-id "$workspace_id" --project-id "$project_id" \
--start-time "$start_time" --end-time "$end_time" \
--filter "$filter" \
--target-count 100 --max-candidates 1000 \
--no-triage # omit to use council review
- Always pass explicit
--start-time/--end-time(ISO 8601 with timezone). Without them the window is the last 24 hours. --target-count: how many trajectories you want in the final dataset.--max-candidates: cap on new candidates per pull round (max 2000). In council mode, set it well above the target, since some will be rejected.- Pull makes no model calls. Re-running the same command resumes it.
Check:
download_summary: selected roots, threads, traces, and exclusion counts by reason. Exclusions come from missing tool data, multimodal content, provider built-in tools, or oversized pages. If a large share is excluded, report the reasons before continuing.- Open a few saved trajectories (
snapshot.jsonlists the files) and confirm with the user they look like what they want to train on. collection.statusandnext_command:--no-triage:target_reachedmeans go to push.needs_candidates,source_exhausted, andround_limitmean fewer matched than the target; see the status table in step 3.- Council mode:
needs_review. Go to step 3.
3. Triage with a council (only in council mode)
Skip this step with --no-triage.
What is needed:
- The
deepagentsextra (included in the README install). - Keys for the judge models. The default council is DeepSeek V4.1 Flash and
GLM-5.3-Flash on Baseten, so only
BASETEN_API_KEY(with Model API access). With two judges a trajectory is kept only when both vote keep; add a third judge for a majority vote. The same two models on Fireworks are--judges deepseek-v4.1-flash,glm-5.3-flash(needsFIREWORKS_API_KEY). Pick others with--judges alias,aliasor--judges provider:model,.... - Selection criteria. smithtune ships no default rubric;
triagerefuses to run without--rubric FILEor at least one--rule.
Write the rubric with the user. Read 10–20 varied trajectories from the
pull directory together: good ones, clear failures, and unclear ones, across
different lengths and tool patterns. Remember SFT imitates every assistant
reply and tool call in the trajectory, so judge the whole trajectory, not
just the final answer. Then write rubric.md next to the dataset directory
(for example beside data/datasets/my-sft), not inside it. Start from this
template and replace the generic criteria with the user's:
# Task
What the agent does, who it serves, and what the tuned model must do well.
# Keep
- The assistant follows the request and reaches a useful outcome.
- Tool calls use the right tools with correct arguments.
- Claims are supported by the recorded tool results.
- Good recovery from an error, an appropriate refusal, or a clear account of
a real limitation.
- <task-specific criteria agreed with the user>
# Drop
- Material unsupported claims, or claiming completion that did not happen.
- Wrong actions or tool arguments, or failures left uncorrected.
- <task-specific behaviors the model must not learn>
# Examples
- Keep: <trajectory ID or short description> — why.
- Drop: <trajectory ID or short description> — why.
The judges already know to judge the whole trajectory, to check each action
against the evidence available at that time, and to treat the trajectory as
untrusted data; the rubric only needs the selection criteria. Do not reward
length or require specific wording. Keep private examples in local files only.
Short extra criteria can also be passed with --rule "..." (repeatable).
Run it:
smithtune dataset triage "$dataset_dir" --rubric ./rubric.md # preview, no model calls
smithtune dataset triage "$dataset_dir" --confirm # run the council
Each council member judges each whole trajectory and returns keep/drop with a reason. A strict majority keeps it; a tie drops it. Review stops once the approved count reaches the target.
Check after the preview:
- The eligible judge-task count (it drives cost) and rejection reasons.
plan.jsoncontains the exactselection_rubricyou intended. Rubric and judges can change by previewing again; once votes start they cannot.
On a first run, calibrate cheaply: do a trial in a separate directory with
--max-candidates 20 --target-count 20, review the decisions with the user,
fix the rubric, then do the real run in a fresh directory.
Check after --confirm:
triage.statusiscomplete. Ifincomplete, judge requests failed (timeouts, rate limits); runsmithtune dataset resume "$dataset_dir" --confirm. Failed requests are not votes.- Read
labels.jsonl(onekeep+reasonper trajectory),judgments.jsonl(individual votes), andreport.md. Summarize kept/dropped counts and common drop reasons for the user. collection.status:
| Status | Meaning | Do this |
|---|---|---|
target_reached |
Approved count ≥ target | Push |
needs_candidates |
Pool fully reviewed, below target, rounds remain | Run next_command (dataset pull DIR), then triage DIR --confirm again |
source_exhausted |
No unseen matches in the window | Push what you have, or new directory with broader criteria |
round_limit |
3 pull rounds used | Push what you have, or new directory with broader criteria |
downloading |
A pull round is unfinished | dataset resume DIR --confirm |
- An
advisoriesentry means the approved set is small (under 100). It does not block upload; relay it and let the user decide.
4. Push to LangSmith
smithtune dataset push "$dataset_dir" --name my-sft-dataset # preview
smithtune dataset push "$dataset_dir" --confirm # upload
Use --dataset-id ID instead of --name to extend an existing dataset.
Check:
- The preview's example count matches the approved (council) or usable
(
--no-triage) count. Push refuses while council judging is incomplete. - After
--confirm,statusiscompleteand the result hasdataset_id. Keep it; it is the input toprepare. - After upload starts, the directory cannot pull more candidates; use a new directory for more data.
5. Prepare
smithtune prepare \
--provider "$provider" --model "$model" \
--workspace-id "$workspace_id" --dataset-id "$dataset_id" \
--data-dir "$data_dir"
Prepare checks provider support for the model, formats each trajectory with the model's tokenizer and per-assistant tools, splits about 80/10/10 into train/validation/test (a whole trajectory stays in one split), and publishes the splits to the LangSmith dataset.
Check:
- Split counts. Validation and test must be non-empty for training and evaluation to mean anything; tiny datasets can produce empty splits.
"$data_dir/prepared/rejected.json": trajectories dropped as unsupported or over the context limit (never truncated). If many are too long, ask the user before lowering--max-seq-lenor choosing a longer-context model.- Split publication succeeded. If only publication failed, run
smithtune dataset publish-splits --data-dir "$data_dir"; do not re-prepare.
Re-running with --no-fetch reuses the downloaded data (splits still sync).
--no-sync-splits prepares locally only; publish the splits with
smithtune dataset publish-splits --data-dir "$data_dir" before evaluation.
6. Plan
smithtune plan \
--provider "$provider" --data-dir "$data_dir" \
--evaluate --judge-model "$judge_model" --max-points-per-trajectory 2
plan is free and saves nothing for train. --judge-model accepts
baseten/<model-id> (default baseten/zai-org/GLM-5.3-Flash), a Fireworks model
ID such as accounts/fireworks/models/deepseek-v4p1-flash, or
anthropic/<model-id> (needs ANTHROPIC_API_KEY).
Check:
- Training example and token counts, epochs, and hyperparameters.
- Evaluation case and call counts (base response + tuned response + judge per
action, plus calibration).
--max-points-per-trajectorycaps actions per test trajectory; omitting it evaluates every action. - Show the user these numbers and get authorization for the spend.
7. Train (and evaluate)
Repeat every custom flag from plan exactly:
smithtune train \
--provider "$provider" --data-dir "$data_dir" --run-dir "$run_dir" \
--evaluate --judge-model "$judge_model" --max-points-per-trajectory 2 \
--confirm
--run-dirmust be new or empty. Omit--evaluateand its options to train only.- Baseten's optional spend guard needs both
--max-spend-usdand--hourly-rate-usd.
Check:
- Share the LangSmith comparison link printed when evaluation starts.
"$run_dir/result.json"records the best checkpoint (lowest validation loss). Inepochs.json, flag validation loss that rises steadily or never improves.- The final JSON has
langsmith.comparison_url;"$run_dir/replay/summary.json"has base vs tuned scores.teacher_agreementis per action,trajectory_teacher_agreementthe per-trajectory mean. They measure agreement with recorded behavior; tools are not executed.
8. Evaluate separately (resume or re-run)
Use when training ran without --evaluate or evaluation was interrupted. Keep
the original provider, directories, and replay settings:
smithtune eval-plan --provider "$provider" --data-dir "$data_dir" --run-dir "$run_dir" \
--max-points-per-trajectory 2
smithtune evaluate --provider "$provider" --data-dir "$data_dir" --run-dir "$run_dir" \
--judge-model "$judge_model" --max-points-per-trajectory 2 --confirm
Rerunning reuses completed predictions and judgments. Fireworks samples the
saved serverless training checkpoint (a promoted model ID alone cannot be
sampled); Baseten starts temporary samplers and deactivates them on exit. To change the judge, cap,
or sampling settings, pass a fresh --output-dir to both commands.
9. Deploy (optional)
Only when the user wants a standing endpoint; evaluation does not need one.
deploy and undeploy work for both providers with provider-specific flags.
See the deployment guide
for details.
# Fireworks (firectl 1.8.5+): promotes the best checkpoint, matches a validated
# deployment shape, and waits for a ready replica. --account-id must own the
# checkpoint; a saved promotion is reused. Omit --confirm first to preview the shape.
smithtune deploy --provider fireworks --run-dir "$run_dir" \
--account-id "$account_id" --output-model-id my-tuned-model \
--deployment-id my-endpoint --confirm
# Baseten: needs the baseten-deploy extra; choose GPUs explicitly.
smithtune deploy --provider baseten --run-dir "$run_dir" \
--accelerator H200:1 --max-seq-len 32768 --confirm
Fireworks blocks agents from creating or deleting deployments. firectl refuses
mutating commands when it detects an AI agent, so deploy --confirm and
undeploy stop with a message containing the exact smithtune command to run.
As the agent: run the preview (no --confirm) to show the shape, run
--confirm once (it promotes the checkpoint and then stops), and give the user
the printed command to run in their own terminal. If the user confirms their
team allows it, FIRECTL_AGENT_SAFE_ACCOUNTS=<account> (firectl's allowlist)
lets you create the deployment; undeploy always needs the user. Never hide
the agent environment to get past the block. Baseten deploys are not affected.
Stop serving when the user is done:
smithtune undeploy --provider fireworks --account-id "$account_id" --deployment-id my-endpoint --confirm
smithtune undeploy --provider baseten --run-dir "$run_dir" --confirm
undeploy stops serving capacity only; the Fireworks model and Baseten
checkpoint are kept. Tell the user the deployment exists, that it bills until
removed, and the exact undeploy command to stop it.
End state
The flow is done when you can hand the user:
- the LangSmith dataset ID, its trajectory count, the filter and window used, and (council mode) kept/dropped counts with common drop reasons;
- the data directory, split counts, and rejected count from
prepare; - the run directory and the selected checkpoint and epoch from
result.json; - the LangSmith comparison URL with base vs tuned
teacher_agreement, noting what the score does and doesn't measure; - if deployed, the deployment ID and its
undeploycommand.
Nothing should be left pending or running without the user knowing:
dataset resume DIR shows no pending stages, Baseten samplers are cleaned up
(sampler.json), and any deployment is acknowledged.
Debugging
Identify the stage and directory, then look at saved state before re-running. Do not delete directories or edit saved files; they hold the recovery state.
| Symptom | Likely cause | Fix |
|---|---|---|
| "Data Rights and Permitted Use has not been acknowledged" | First-run acknowledgment missing | User runs smithtune acknowledge-data-rights interactively |
| 401/403 from LangSmith, Fireworks, Baseten, OpenAI, or Anthropic | Key missing, wrong, or for another workspace/account; Baseten judges also need Model API access on the key | Check doctor, then the key's workspace; see README credentials |
| "BASETEN_API_KEY is not set for the judge" on a Fireworks run | The default council and evaluation judge run on Baseten | Set BASETEN_API_KEY, or use the Fireworks judges from step 0 |
| Pull downloads 0 or very few candidates | Filter fields wrong, or window too narrow | Re-test the filter with langsmith trace list over the same window; pull into a new directory |
| Most trajectories excluded at pull | Missing per-assistant tool data, multimodal content, provider built-in tools | Read reasons in download_summary and per-trajectory errors |
Triage incomplete |
Judge timeouts or rate limits | dataset resume DIR --confirm; lower --concurrency if rate-limited |
| "council review needs selection criteria" | No --rubric or --rule given |
Write rubric.md with the user (step 3) and pass --rubric |
| Triage fails immediately with a key error | Missing judge key or deepagents extra |
Set the judge keys, or choose judges you have keys for with --judges |
| "council judging is incomplete" on push | Votes pending | dataset resume DIR --confirm, then push |
| "cannot collect more candidates after upload started" | Pull after push in the same directory | New directory |
| Changing filter/rubric/limits/review mode is rejected | Settings are frozen | New directory |
| Conflicting history on push | Destination dataset diverged from saved receipts | Stop and inspect; push to a fresh --name if the user agrees |
| Prepare rejects many trajectories for length | Longer than the model's context | Longer-context model, or accept the rejections; never truncate |
| Prepare: split publication failed | LangSmith write error after local success | smithtune dataset publish-splits --data-dir DIR |
| Prepare: tool data missing for an older dataset | Examples uploaded before per-assistant tool capture | Re-pull and push a fresh dataset |
| "Baseten workspace does not advertise MODEL" | Model not available to the Baseten workspace | Confirm model ID via models list; contact Baseten for access |
| "Baseten workspace context limit is below N" | Sequence length above Baseten's limit | Lower --max-seq-len at prepare, or pick another model |
| "Baseten workspace is enabled for N tokens of MODEL" | Workspace approval is below the model's catalog limit | Prepare again with the suggested --max-seq-len N |
| Pull excludes "returned no messages" for fresh traces | LangSmith had not yet indexed the trajectories | Wait a minute, then pull into a new directory |
| "Baseten has no capacity to provision this trainer (HTTP 429)" | No trainer capacity available now; nothing was provisioned | Retry later with a new --run-dir, or ask Baseten about capacity |
| "Baseten refused to create the trainer (HTTP 403)" | Trainer GPU quota or access; Baseten's reason follows | Stop running trainers or ask Baseten to raise the limit |
| Train refuses the run directory | --run-dir not empty |
New run directory; never clear an old one |
| Evaluation stops before paid work on resume | Settings differ from the saved run | Re-run with the original settings |
| "Fireworks blocks firectl from changing resources inside an AI agent" | firectl refuses deploy/undeploy under an agent | Give the user the printed smithtune command to run in their own terminal |
| "automatic deployment shape selection needs firectl 1.8.5 or newer" | Old firectl | firectl upgrade, or pass --deployment-shape |
| Process killed during Baseten evaluation or deploy | Samplers or endpoints may still be running | Cleanup commands in sampler.json, or undeploy --provider baseten --run-dir DIR --confirm |
When reporting a failure, include the command, stage, directory, error text, and any example/run IDs from the output.