Desktop request reference
ster request <operation> runs one JSON request to completion and exits: the body on stdin, NDJSON log events and one result event on stdout, and the result's status as the exit status. Ster Desktop runs one per operation.
Overview#
ster request <operation> is how a desktop app runs Ster without building argv for the CLI (src/request/). It reads one JSON document on stdin, runs the operation with the same functions the CLI command runs, prints NDJSON events on stdout, and exits. Ster Desktop starts one per operation, so nothing of Ster stays resident between two operations and no port is opened. It replaces ster serve, the loopback HTTP backend the desktop used to keep running for as long as its window was open.
Lifecycle#
$ echo '{"artifact": "calm.ster.json"}' | ster request vector/inspect- The body is read to the end of stdin before anything runs; an empty or whitespace-only body is
{}, so every choice takes its default and an operation that needs a number refuses it by name. - stdout carries only events, one JSON object per line, each flushed as it is written.
- The process exits with the result's
statusonce the operation has finished. A reader that closes stdout ends the request at its next event, so a training run does not outlive the window that asked for it. - There is no listener, no port and no job queue: each request is its own process, and two requests are two processes.
The consequence is worth stating plainly: Ster cannot reach a model that does not fit the machine it is running on. Every path in every request — model, pairs, vector, adapter, output — is resolved on the filesystem of the ster request process, relative to its working directory, and the desktop app runs that process on its own Mac. There is no host or remote field on any request. A checkpoint too large for this Mac's memory is out of reach here: run Ster on the machine that can hold the weights, and use that machine's own CLI or its own desktop app.
Operations#
Anything else is refused before it runs as unknown operation: <name>.
| Operation | Body | Mirrors |
|---|---|---|
vector/train | TrainRequest | ster vector train |
vector/optimize | OptimizeRequest | ster vector optimize |
vector/curve | CurveRequest | ster vector curve |
vector/evaluate | EvaluateRequest | ster vector evaluate |
generate | GenerateRequest | ster generate |
extract | ExtractRequest | ster extract |
parity | ParityRequest | ster parity |
vector/inspect | InspectRequest | ster vector inspect |
vector/compare | CompareRequest | ster vector compare |
vector/project | ProjectRequest | ster vector project |
vector/ablate | AblateRequest | ster vector ablate |
decide | DecideRequest | ster decide |
calibrate | CalibrateRequest | ster calibrate |
workspace/import-pairs | WorkspaceImportPairsRequest | ster workspace import-pairs |
pairs/inspect | PairsInspectRequest | ster pairs inspect |
pairs/merge | PairsMergeRequest | ster pairs merge |
pairs/save | PairsSaveRequest | PairSet::save, the validated write behind ster pairs add and ster pairs remove |
pairs/synthesize | PairsSynthesizeRequest | ster pairs synthesize |
tune/sft | TuneSftRequest | ster tune sft |
tune/dpo | TuneDpoRequest | ster tune dpo |
tune/reward | TuneRewardRequest | ster tune reward |
tune/grpo | TuneGrpoRequest | ster tune grpo |
tune/merge | TuneMergeRequest | ster tune merge |
tune/evaluate | TuneEvaluateRequest | ster tune evaluate |
tune/inspect | TuneInspectRequest | ster tune inspect |
Every operation reuses the exact functions the CLI commands use (workflow, pairs, tune, lora, runtime, artifact) and returns the same document the CLI prints. pairs/save is the one operation with no CLI command of its own: the desktop editor sends a whole set in one request instead of one add per pair.
Events and exit status#
- Zero or more
{"type":"log","stream":"stderr","chunk":"..."}events — the same progress lines the CLI writes to stderr, delivered through the workflow progress sink. - Exactly one final
{"type":"result","status":<status>,"json":{...}}event, wherejsonis the document the CLI prints andstatusis the exit status of the process.
| Status | Meaning | json |
|---|---|---|
0 | The operation completed. | The document the CLI command prints. |
1 | The operation started and failed. The last log event is error: <sentence>, the sentence the CLI prints after Error:. | {"error": "<sentence>"} |
2 | The request was refused before anything ran: an unknown operation, a body that does not parse (request body is not valid JSON: <reason>), or a missing or invalid field. No log event precedes it, and no model loads. | {"error": "<sentence>"} |
vector/optimize returns the vector/train document with one selection object added, carrying holdout (fraction, fit_pairs and holdout_pairs) and one candidates row per layer and method with its holdout_accuracy, holdout_margin, and a selected flag that is true on exactly one row; its fitting each candidate on 6 pairs and ranking on a 2-pair holdout line arrives as a stderr log event, as does the one-pair-holdout warning when the split leaves one pair (`ster vector optimize`). vector/inspect returns the artifact object alone, with no outer chat_template, because it loads no model and so has no encoding of its own to report. vector/evaluate streams the provenance warning the CLI prints when the artifact was fitted in a different space than the request encodes in. vector/compare returns the comparison `ster vector compare` prints and, like vector/inspect, loads no model.
Request shapes#
Fields are camelCase. An empty or whitespace-only body is treated as {}. Choices with a default (device, layers, method, chatTemplate, precision, targets, loss, reward, turns, warmupSteps, permutations) are filled in and validation reports a missing path or text; every training, sampling and judging number has no default — Ster assumes none — so a body without one is refused at deserialization, naming it: request body is not valid JSON: missing field \learningRate\ at line <L> column <C>. File-path fields (pairs, output, vector, adapter, input, examples, artifact, path) are paths on the filesystem of the ster request process, resolved relative to its working directory.
Shared model fields (every operation except vector/inspect, vector/compare, pairs/inspect, pairs/save, and tune/inspect): model (required), revision (optional), device (default "cpu"). On pairs/synthesize they belong to the local route alone: model is required when generator is "local" and unused when it is "brama", which loads no weights. precision sits beside them on every request that maps a checkpoint — the same six plus the five tune/* training operations — defaulting to "f32"; tune/merge and tune/inspect declare none (precision). chatTemplate sits beside it on exactly the same eleven requests, taking "auto" or "off" and defaulting to "auto": both name a property of the space a run reads and writes in, so the operations that have one have the other (chat template).
| Request | Fields beyond the model block | Defaults (fields marked † are required numbers) |
|---|---|---|
| Train | pairs, `output`, layers, method, chatTemplate, precision | layers: "all", method: "caa", chatTemplate: "auto", precision: "f32" |
| Optimize | pairs, `output`, holdout†, layers, chatTemplate, precision | layers: "all", chatTemplate: "auto", precision: "f32" |
| Curve | pairs, `method`, holdout†, sizes† (whole numbers of pairs), layers, chatTemplate, precision | layers: "all", chatTemplate: "auto", precision: "f32" |
| Evaluate | pairs, `vector`, chatTemplate, precision | chatTemplate: "auto", precision: "f32" |
| Generate | prompt, `steering` (each entry `vector`, strength†), adapter, maxNewTokens†, temperature†, topP, seed†, chatTemplate, precision | chatTemplate: "auto", precision: "f32"; an absent or empty steering generates unsteered, several entries steer several traits at once, an entry without strength is refused at deserialization naming it and one without vector as generate names a steering part with no vector; empty adapter means the plain base weights, absent topP samples the whole distribution |
| Extract | input, `output`, layers, chatTemplate, precision | layers: "all", chatTemplate: "auto", precision: "f32" |
| Inspect | artifact* | — |
| Compare | artifacts* (two or more paths), clusters†, layers (whole numbers) | absent layers compares every layer all the artifacts carry |
| Project | pairs, `vector`, layer†, strength†, components†, chatTemplate, precision | chatTemplate: "auto", precision: "f32" |
| Ablate | model, `revision`, `vector`, strength†, output* | no precision or chatTemplate, and device is unused: the tensors are rewritten on the CPU without building a decoder |
| Pairs inspect | pairs*, dedupeBits†, dedupeBands†, refusalThreshold†, unbalancedRatio† | — |
| Pairs merge | sources (two or more paths), `traitName`, output* | — |
| Pairs save | path, `traitName`, `entries` (each entry positive, negative) | traitName: ""; at least one entry |
| Pairs synthesize | generator, `generatorModel`, `trait`, count, `output`, traitName, opposite, retryMultiplier†, dedupeBits†, dedupeBands†, refusalThreshold†, maxNewTokens†, temperature†, topP†, seed†, chatTemplate, precision | chatTemplate: "auto", precision: "f32" |
| Tune sft | examples\, `output`\, rank†, alpha†, targets, layers, epochs†, learningRate†, accumulation†, warmupSteps, maxSequence†, seed†, chatTemplate, batchSize†, precision | targets: "query,value", layers: "all", warmupSteps: 0, chatTemplate: "auto", precision: "f32" |
| Tune dpo | pairs\, `output`\, rank†, alpha†, targets, layers, beta†, loss, epochs†, learningRate†, accumulation†, warmupSteps, maxSequence†, seed†, chatTemplate, batchSize†, precision | targets: "query,value", layers: "all", loss: "dpo", warmupSteps: 0, chatTemplate: "auto", precision: "f32" |
| Tune reward | pairs\, `output`\, rank†, alpha†, targets, layers, epochs†, learningRate†, accumulation†, warmupSteps, maxSequence†, seed†, chatTemplate, batchSize†, precision | targets: "query,value", layers: "all", warmupSteps: 0, chatTemplate: "auto", precision: "f32" |
| Tune grpo | prompts\, `output`\, reward\*, group†, iterations†, beta†, rank†, alpha†, targets, layers, learningRate†, accumulation†, warmupSteps, maxNewTokens†, temperature†, topP†, maxSequence†, seed†, chatTemplate, precision | reward: "length", targets: "query,value", layers: "all", warmupSteps: 0, chatTemplate: "auto", precision: "f32" |
| Tune merge | adapter\, `output`\ | — |
| Tune evaluate | examples\*, adapter, maxSequence†, chatTemplate, batchSize†, precision | chatTemplate: "auto", precision: "f32" |
| Tune inspect | artifact\* | — |
\* validated as required. Each missing required field has one fixed refusal sentence, e.g. vector/train requires a pairs file, generate requires a prompt, vector/inspect requires a steering artifact, vector/compare needs at least two artifacts and got 1, pairs synthesize requires a trait description — the full list is in the runbook.
decide carries the decision document itself under request (state and questions, exactly the document `ster decide --request` reads) beside the shared model fields, plus chatTemplate, precision, permutations (default 0, every option order), explain (default false) and an optional calibration path. Its status-2 refusals are decide requires a model, decide requires a request with a state and questions, and then the request's own sentences, choice question '<id>' needs at least two options and the rest listed in the runbook. The result event's json is the response document ster decide prints: answers keyed by question id, usage, and explain when asked for. calibrate takes examples and output as paths and the required eceBins with the same model fields and permutations; it refuses a body without eceBins by name, calibrate requires a model, calibrate requires an examples path and calibrate requires an output path, writes the artifact to output, and returns it as the result's json (`ster calibrate`). Both stream progress lines — reading the state once (84 tokens), then 8 question renderings (380 tokens between them) — as stderr log events like every other operation.
On pairs/synthesize, chatTemplate belongs to the local route with the rest of the model block: "brama" is already a chat API, so the field is accepted and ignored there. precision behaves the same way for the same reason — that route loads no weights.
Pair set operations#
The three pairs/* operations are the desktop's pair-set surface: one audit, one write, one generator. They print events like every other operation — zero or more log events, then exactly one result. The bodies below are field shapes, not recorded sessions.
pairs/inspect audits a set on disk and loads no model:
{ "pairs": "pairs.json", "dedupeBits": 3, "dedupeBands": 8,
"refusalThreshold": 0.5, "unbalancedRatio": 3.0 }The result is the document `ster pairs inspect` prints: trait_name, pair_count, duplicate_count, refusal_count, unbalanced_count, unbalanced_ratio, diversity, and one entries record per pair. Refusals before it runs: a missing number by name, and pairs inspect requires a pairs file.
pairs/save is the editor's write path — a whole set in one request, validated before anything reaches disk:
{ "path": "pairs.json", "traitName": "calm",
"entries": [ { "positive": "the sea is calm and quiet tonight .",
"negative": "the storm waves crash loud against the rocks ." } ] }The result is {"path": "pairs.json", "pairCount": 1}. Refusals before it runs: pairs save requires an output path and pairs save requires at least one pair. A set the loader would reject never reaches disk, and the desktop sees the same sentence the CLI prints, for example pair set pairs.json contains an empty positive or negative prompt.
pairs/synthesize writes the generated set and reports on the run. generator selects the route and is required — a body without it is refused with pairs synthesize requires a generator, local or brama; the shared model block belongs to that route alone, so model is required on "local" and absent on "brama", which takes generatorModel instead and reads BRAMA_URL and BRAMA_BEARER from the environment of the ster request process — the environment Ster Desktop itself was started with:
{ "generator": "local", "model": "toy-llama", "trait": "calm and unhurried",
"count": 20, "output": "calm-pairs.json", "retryMultiplier": 3,
"dedupeBits": 3, "dedupeBands": 8, "refusalThreshold": 0.5,
"maxNewTokens": 96, "temperature": 0.9, "topP": 0.95, "seed": 42 }The same request on the hosted route names no model at all:
{ "generator": "brama", "generatorModel": "wisent-backend/chat/primary",
"trait": "calm and unhurried", "count": 20, "output": "calm-pairs.json",
"retryMultiplier": 3, "dedupeBits": 3, "dedupeBands": 8,
"refusalThreshold": 0.5, "maxNewTokens": 96, "temperature": 0.9,
"topP": 0.95, "seed": 42 }The result is {"path": "calm-pairs.json", "report": { … }}, where report is the synthesis report (synthetic pairs) whose first field, generator, is local:<model id> or brama:<route>. Each attempt's synthesizing pair 3/20 (attempt 7) line arrives as a stderr log event, which is what a desktop progress display reads. Refusals before it runs: pairs synthesize requires a model on the local route, pairs synthesize with the brama generator requires generatorModel on the hosted one, and unknown generator; expected local or brama for any other value, then pairs synthesize requires a trait description, pairs synthesize requires an output path, and pairs synthesize requires a pair count above zero on both. A zero temperature is refused once the operation has started, as synthesis requires a temperature above zero; argmax generation repeats a single prompt; Brama's own refusals surface the same way, once it is running.
Tune operations#
The seven tune/* operations are the desktop's fine-tuning surface: four objectives that train an adapter, and three utilities that read one. They print events like every other operation — zero or more log events, then exactly one result — and every one calls the same functions the CLI arms call, so there is no second implementation to drift. Every training number — rank, alpha, learningRate, accumulation, maxSequence, seed, and epochs/batchSize or GRPO's sampling settings — is required, exactly as its flag is. The four objectives and tune/evaluate take chatTemplate, "auto" or "off", defaulting to "auto" exactly as the --chat-template flag does, and so do the six operations outside this group that turn prose into tokens — vector/train, vector/optimize, vector/evaluate, extract, generate and pairs/synthesize; the four that fold rows into a forward pass take batchSize. The same five also take precision — "f32", "f16" or "bf16", defaulting to "f32" — naming the dtype the frozen base weights are mapped at and nothing a run trains (precision).
tune/sft trains LoRA adapters on prompt and completion examples:
{ "model": "toy-llama", "examples": "examples.json",
"output": "calm.lora.safetensors", "rank": 8, "alpha": 16,
"epochs": 3, "learningRate": 0.005, "accumulation": 4,
"maxSequence": 512, "batchSize": 1, "seed": 42 }An instruct checkpoint wants its own conversation format, and a batch folds several examples into one forward pass without changing the loss:
{ "model": "toy-chat-model", "examples": "chat-examples.json",
"output": "calm.lora.safetensors", "layers": "0", "rank": 8, "alpha": 16,
"epochs": 1, "learningRate": 0.0001, "accumulation": 8,
"maxSequence": 64, "chatTemplate": "auto", "batchSize": 4, "seed": 42 }On the toy checkpoint with a chat template beside it, that request streams applying the model's own chat template to every prompt and completion as its first log event and returns a report whose chat_template is "applied" and whose batch is 4, at a final loss of 4.1717 — the same loss the identical run reports at batchSize: 1, because a batch changes how many sequences share a kernel launch and not what is being optimized. The same eight examples at "chatTemplate": "off" report 4.2572. The numbers it did not name were the defaults of the time, written out above; Ster no longer has them.
tune/dpo trains them on a preference instead, over the contrastive pair set `vector/train` already reads. loss selects "dpo" or "ipo":
{ "model": "toy-llama", "pairs": "pairs.json",
"output": "calm-dpo.safetensors", "beta": 0.1, "loss": "dpo",
"rank": 8, "alpha": 16, "epochs": 3, "learningRate": 0.005,
"accumulation": 4, "maxSequence": 512, "batchSize": 1, "seed": 42 }tune/reward trains a scalar head on the same pair set and writes an artifact of kind: "reward":
{ "model": "toy-llama", "pairs": "pairs.json",
"output": "calm.reward.safetensors", "rank": 8, "alpha": 16,
"epochs": 3, "learningRate": 0.005, "accumulation": 4,
"maxSequence": 512, "batchSize": 1, "seed": 42 }tune/grpo samples a group of completions per prompt and runs a policy gradient against a reward. reward is the keyword "length" or a path to a reward artifact, and it is resolved before the policy is loaded:
{ "model": "toy-llama", "prompts": "prompts.json",
"output": "calm-grpo.safetensors", "reward": "length",
"group": 4, "iterations": 3, "beta": 0.04, "rank": 8, "alpha": 16,
"learningRate": 0.005, "accumulation": 1, "maxNewTokens": 16,
"temperature": 0.9, "topP": 0.95, "maxSequence": 512, "seed": 42 }Those four each return {"path": "…", "report": { … }}, where report is the same document the matching CLI command prints and the same one recorded inside the artifact's sidecar.
tune/merge folds an adapter into the base weights and writes a checkpoint directory:
{ "model": "toy-llama", "adapter": "calm.lora.safetensors",
"output": "./calm-merged" }It returns {"report": { … }} with no path, because the answer is a directory and the report's output and files name it.
tune/evaluate scores a checkpoint and writes nothing. adapter is optional, and an empty string means the bare checkpoint:
{ "model": "toy-llama", "examples": "examples.json",
"adapter": "calm.lora.safetensors", "maxSequence": 512, "batchSize": 1 }It returns the report itself rather than a {path, report} wrapper, because nothing was written. Its fields are snake_case where the request's are camelCase — the report is the Rust struct serialized as-is, exactly as every other operation's report is — so a client reads loss, perplexity, mean_example_loss, mean_example_perplexity, completion_tokens, and one entries record per example.
tune/inspect audits an artifact on disk and loads no model:
{ "artifact": "calm.reward.safetensors" }The result is the document ster tune inspect prints, under one adapter key: identity including kind, rank, alpha, the derived scale, targets, layers, hidden_size, one name and shape record per tensor — including reward.head on a reward artifact — and train, the recorded report.
Refusals before the operation starts are status-2 results: a missing training number by name (request body is not valid JSON: missing field \rank\ at line <L> column <C>), then tune sft requires a model, tune sft requires an example set, tune sft requires an output path; tune dpo requires a model, tune dpo requires a pairs file, tune dpo requires an output path; tune reward requires a model, tune reward requires a pairs file, tune reward requires an output path; tune grpo requires a model, tune grpo requires a prompt set, tune grpo requires an output path, tune grpo requires a reward source; tune merge requires a model, tune merge requires an adapter, tune merge requires an output directory; tune evaluate requires a model, tune evaluate requires an example set; and tune inspect requires an adapter artifact.
Everything after that is a failure while running and mirrors the CLI: the refusal sentence on a stderr log event, then one result with status 1. unknown preference loss "kto"; expected dpo or ipo, unknown chat template mode "on"; expected auto or off, unknown precision "fp16"; expected f32, f16, or bf16, bf16 has no CPU matmul kernel in this Candle build; use --precision f16 for half precision on cpu, or --device metal for bf16, supervised fine-tuning requires a batch of at least one example, and adapter artifact is a reward model, not a generation adapter arrive that way.
generate takes the same adapter: an adapter path attaches a frozen generation adapter while the weights are mapped, and refuses a reward artifact.
Workspace import#
workspace/import-pairs accepts { "source": "/absolute/pairs.json", "name": "optional-id" } and calls the same product-owned import operation as the CLI. It returns an ImportReport with imported, unchanged, conflicting, or rejected; the active id and indexed destinations are what `ster workspace show` prints. The desktop uses this operation rather than parsing pair JSON itself.