# Changelog Source: https://docs.freesolo.co/changelog New models, faster training and serving, and platform updates from the Freesolo team. ## Featured ### Serve in your own cloud account `flash serve deploy` provisions serving in **your own Modal or RunPod account** for one base model and one run's adapter. The GPU runs and bills in your account, and you call the provider's HTTPS endpoint directly, with no Freesolo gateway in the request path. `flash serve status` proves the current state without touching it, and `flash serve undeploy` removes one deployment and proves its resources are gone. Provider credentials are request-only: read from the environment for one call and never stored, logged, or written into the deployment record. The serving image must be pinned to a digest, so a deployment cannot drift to different content after it is recorded. See [Serve in your own account](/guides/deploy-and-chat#serve-in-your-own-account). ### Warm start works between any two algorithms `[train] init_from_adapter` now accepts a source adapter from **any** algorithm, for **any** target. SFT was previously rejected as a warm-start target; all nine combinations of `sft`, `grpo`, and `opd` now work, including same-algorithm continuation such as `sft` → `sft` to keep training on more data. A warm-started SFT run also inherits its source's base-model pin, so the continued adapter stays deployable when the upstream base moves. See [Warm-start safely](/guides/training#warm-start-safely). ### Retried runs say which worker spoke `flash runs log` names the attempt each log section came from and tags heartbeats from a superseded or torn-down worker, so skimming to the last heartbeat can no longer report a dead worker's final lines as live progress. Log artifacts are kept even when their attempt cannot be identified. * **Training:** GRPO `group_size` is exactly `2`, `4`, or `8` (default `8`), and a step is capped at 512 completions (`prompts_per_step x group_size`). Both are checked at submit, before a GPU is allocated, and Flash never rewrites the value you authored. See [the GRPO rollout shape](/reference/configuration#the-grpo-rollout-shape-is-a-fixed-set). * **GPUs:** A multi-card quote now shows pooled VRAM against the whole-run requirement (`300 GB usable across 2x 180 GB; run needs >= 199 GB`), so a shape that fits no longer reads as a rejection. A single-card pin that cannot hold the run names the card count that would. * **Cost:** SFT `--cost` and `--dry-run` name the published environment and commit their counts came from, and say to republish when the numbers look stale. Inline `records` are labelled as coming from your config. * **Runs:** A failed checkpoint upload reports its cause in the run heartbeat and log instead of leaving a silent gap; a later successful upload of the same checkpoint clears it. * **Runs:** A CUDA out-of-memory failure keeps the allocation figures (how much was requested against what the card had), and the bare `CUDA error: out of memory` spelling is now classified. * **CLI:** An unknown flag comes back with the closest real flag for the command you ran, and says when a flag belongs at the root instead of on the subcommand. * **CLI:** `flash env setup` fills in the `[wandb]` block with your project's name and a folder-derived run name; edit or delete it freely. ## Featured ### SFT estimates return in the same command Flash 1.2.63 returns SFT `--cost`, `--dry-run`, and submit preparation synchronously from the selected packaged dataset, without a separate profile job or training GPU allocation. The estimate is static, so environment-defined prompt construction, filtering, and transformations can still change real training. See [Cost and billing](/reference/cost-model). ### Multimodal training has explicit contracts Multimodal training now has explicit input limits and algorithm-specific rules; serving has a separate request contract. See [Image inputs](/guides/datasets#image-inputs) and [Deploy & chat](/guides/deploy-and-chat). ### Deployments are easier to call and harder to confuse The default `flash models deploy --wait` timeout is now 2400 seconds, and runs have an ownership-checked `POST /v1/runs/{run_id}/chat` route. Each run has one shared bare alias, so deploying another checkpoint moves that alias. See [Deploy & chat](/guides/deploy-and-chat). * **Training:** Multi-turn SFT masks non-assistant target turns when role parsing succeeds. See [Multi-turn SFT masking](/guides/datasets#multi-turn-sft-masks-observations). * **Configuration:** Delete `model_revision` from older configs; current clients reject it and Flash resolves supported revisions internally. * **GPUs:** GPU type lists are acceptable classes ranked by cost; `[gpu] providers` is an ordered preference, while scalar `type` and `provider` values remain hard pins. * **Self-hosting:** Use a standalone GitHub source reachable from the plane and resolving to `environment.py`; managed Hub slugs are not accepted. * **CLI:** `flash env eval TARGET` uses the run's published environment, supports split and parameter overrides, and uploads by default. `flash runs status --json` prints one JSON object, or JSONL with `--follow`. * **Serving:** Deploying a checkpoint warns before moving the shared bare run alias to that checkpoint. ## Featured ### Runs size their own GPUs Leave `[gpu] count` out and Flash now picks the smallest shape your run actually fits on, then ranks the fitting shapes by cost per step. Configurations that used to be rejected for needing more memory than a single card holds - a large model at long context, or at a high LoRA rank - now land on two or more cards instead of failing. Setting `count` (or `--gpus N`) still pins a ceiling that never escalates, and pinning `[gpu] type` without a count stays a single-card pin, so naming a specific card never quietly bills you for four of them. When a run does not fit, the error names the smallest count that would work. See [Multi-GPU runs](/reference/configuration#multi-gpu-runs). ### The RL optimizer batch has its own name GRPO and OPD now author `prompts_per_step`; `batch_size` is SFT-only, and each is rejected under the other algorithm. They were never the same quantity. Under SFT a measured workload profile turns `batch_size` into examples-per-update; under GRPO and OPD the key **is** the optimizer batch. So `batch_size = 1`, the standard SFT out-of-memory workaround, silently meant one prompt per update when copied into an RL config - the run trained, logged, and billed at a fraction of the intended batch without erroring. Porting an older RL config is a rename, not a copy: the defaults are `64` for GRPO and `8` for OPD. See [the optimizer batch](/reference/configuration#the-optimizer-batch-has-a-different-name-per-algorithm). * **Environments:** Environment names are now unique **per project**, not per organization. Two projects in one organization can each publish their own `math`. The owning project is part of the id, which is now `namespace/project/name`, and renaming a project does not change ids it already published. Existing environments keep their name and are republished under their owning project's slug. **Update your configs:** a two-segment `namespace/name` id no longer resolves, so replace it with the new id - `flash env list` prints the full id of everything you have published. * **Environments:** `[environment] pip` is a config key again - declare the third-party packages your scorer imports and the worker installs them. Entry **syntax** is validated at submit, so a malformed or disallowed requirement fails before a GPU is allocated. A well-formed entry that cannot resolve - a misspelled name, an impossible version pin - still fails during install, after allocation. Pip options and URLs carrying credentials are rejected. See [Scorer dependencies](/reference/configuration#scorer-dependencies). * **Flash CLI:** `flash env list` now shows your organization's published environments alongside your local sources, so the id you paste into `[environment] id` is in front of you. * **Environments:** `flash env list` prints the full id of every published environment, which is the value to paste into `[environment] id` after this change. * **Training:** An OPD run whose rollouts are nearly all truncated now fails naming `max_completion_tokens` and the truncated fraction, rather than exiting on an opaque subprocess status. * **Training:** SFT sequence packing now covers gated-delta-net models, which every catalog model is, once the training image can reset example boundaries. Previously those runs trained exactly one example per update, so a configured `batch_size` did not group anything. Runs that still cannot pack - multimodal rows - warn about that. * **Training:** A transient package-index failure while installing your environment's dependencies is retried instead of failing the run. A real resolution or build failure stays terminal rather than burning GPU time on a retry that cannot succeed. * **Cost:** A GRPO or OPD config that states neither `max_steps` nor `max_examples` is now refused rather than quoted. The horizon came from the retained prompt pool, so an unbounded pool derived a single step and previewed a full run at one step's price. * **Self-hosting:** Flash now also installs a `flash-cli` console script. The `server` and `dev` extras pull in `runpod-flash`, which claims the same `flash` name, so on a plane host `flash` could silently be a different tool. `flash-cli` always reaches Flash. ## Featured ### Distil from image prompts On-policy distillation accepts image-bearing prompts when the teacher can actually see them. Pick an image-capable `teacher_model` - `qwen3.5-397b-a17b` or the new `qwen3-vl-235b` - and a text-only teacher is refused rather than silently distilling from a teacher that never saw the pixels. Single-turn only. See [image-bearing OPD](/reference/configuration#image-bearing-opd). * **Environments:** On the managed service, `[environment] id` takes a published `namespace/name` slug only; `github:` refs and GitHub URLs are rejected at submit and remain available when self-hosting. * **Self-hosting:** The OPD teacher-broker origin is now set with `FLASH_PUBLIC_URL`; the former `FLASH_CONTROL_PANEL_URL` is no longer read. It is the plane's own worker-reachable origin, not the client's `--api-url`. * **Self-hosting:** `flash login` warns when given a non-loopback `http://` URL, which would send the plane's root credential in cleartext. ## Featured ### SFT cost comes from a measured profile, not a guess SFT cost depends on the exact tokenized dataset, so `--cost` no longer estimates it from a row count. The first `--cost`, `--dry-run`, or `train` for a config starts a short profile run that loads your environment and tokenizes exactly the rows training would consume, then quotes from that. It is measured once per exact config and reused, so only the first submission waits. Superseded in Flash 1.2.63 on August 17, 2026. SFT estimates now read the packaged dataset synchronously without importing `environment.py`, creating a profile run, or allocating a training GPU. This entry remains as release history. See [Cost and billing](/reference/cost-model#grpo-and-opd-quote-locally-sft-reads-packaged-data). ### `[train]` rejects knobs that do not apply to your algorithm Setting `group_size` on an SFT run used to be silently ignored, which reads as a setting that did nothing. Flash now rejects a knob the chosen algorithm never consumes, and names the algorithm in the error, so a typo fails at parse time rather than after a GPU is rented. See [Knobs are scoped by algorithm](/reference/configuration#knobs-are-scoped-by-algorithm). ### Run Flash on your own GPU accounts A self-hosted control plane now works against any subset of RunPod, Lambda, and Vast: configure the providers you have and the allocator only ranks those. Startup preflights the operator config and refuses to boot if it could not run a job, hosted-only CLI commands either work locally or say why they cannot, and the state directory, logging, and bind address are configurable. See [Self-hosting](/guides/self-hosting). * **Training:** `lora_alpha` is authorable again in `[train]`, defaulting to `2 * lora_rank`. Omit both it and `lora_rank` when warm-starting from an adapter - they are inherited. * **Flash CLI:** `flash train --gpus N` requests a card count without editing the config. The count is a ceiling, and Flash rents the largest rentable shape at or below it. * **Models:** 4B and 9B now serve LoRA rank 128, matching what the serving app actually supports, and every catalog model accepts a 32768-token context. * **Models:** Every trainable model must be in the catalog. An uncatalogued `model` is rejected when the config is parsed, before any GPU is allocated. * **Environments:** The worker installs one managed requirement set for every environment, so `[environment] pip` and the per-run `[worker_env]` table are no longer accepted. Import from the `freesolo` SDK and the standard library. * **Training:** Multi-card runs are priced for every card they occupied, and NVLink scaling is only credited on combinations that actually have it. ## Featured ### Evaluate a deployed model against held-out suites Put an `evaluations.py` beside your `environment.py` and `flash env eval RUN_ID` scores those suites against the deployed adapter. Cases that never reached the model are counted as errors instead of averaged in as zeros, so a half-broken deployment reads as broken rather than as a weak model. Available in Flash 1.0.34. ### Gate a script on a deploy instead of polling `flash models deploy --wait` blocks until the revision is actually servable and exits non-zero if it fails, so a deploy can be chained in automation. * **Environments:** `flash env test` can drive the split you actually train on with `--split` and `--param`, and now fails when every replayed gold answer scores zero. * **Environments:** Scope an eval with `--suite`, cap it with `--max-cases`, and raise `--concurrency` to run cases in parallel. * **Serving:** Interrupting `flash models deploy --wait` stops waiting, not the deployment. * **Flash CLI:** `flash train --dry-run` checks your org balance against the quote, so an insufficient balance fails before you submit. * **Flash CLI:** `flash env setup` scaffolds a starter `evaluations.py`. ## Featured ### Turn your app's live traffic into a training set Point an OpenAI-compatible client at `https://api.freesolo.co/v1` and Freesolo records your app's chat completions against a project, then hands them back as a training dataset with `flash traces export`. Secrets are redacted before anything is forwarded or stored. See [Tracing](/guides/tracing). ### Thinking models split reasoning out of `content` Reasoning now comes back in `reasoning_content` and only the answer in `content`, in both JSON and streaming responses, so the `` separator no longer leaks into `content`. * **Serving:** `flash models chat` folds the two fields back into one `...` block, so the CLI still shows the full generation. ## Featured ### Every run and environment belongs to a project Training configs take a required top-level `project` UUID, and `flash env push` and `flash env delete` take a required `--project`. Flash validates the project against your organization before allocating a GPU, so a wrong or missing project fails the submit instead of starting paid work. Available in Flash 1.0.25. ### Run and serving commands are grouped `flash status`, `log`, `cancel`, and `checkpoints` are now `flash runs status`, `runs log`, `runs cancel`, and `runs checkpoint`. `flash deploy`, `chat`, `undeploy`, `deployments`, and `export` are now `flash models deploy`, `models chat`, `models undeploy`, `models deployments`, and `models export`. Listing base models is `flash models list`. * **Platform:** Deploy and tear down serving from the dashboard, without switching to the CLI. * **Platform:** A project selector in the top bar moves between projects from anywhere in the dashboard. * **Flash CLI:** Create and find projects with `flash projects create` and `flash projects list`. ## Featured ### Organize runs, environments, and traces into projects Group related work under an organization-level project and switch between projects from the platform. ## Featured ### Watch training as it happens Live metrics, run logs, and sample completions in the platform, plus per-step rewards from the Flash CLI. ## Featured ### 7-10x faster generation on 9B and 27B models ### SFT training up to 48% faster Lower memory use and faster data processing also unlock longer training contexts. * **Training:** Pure multi-turn GRPO tasks with independently gradable turns can choose per-turn or per-episode credit. ## Featured ### Train and serve multimodal models Use image inputs with multimodal SFT, GRPO, on-policy distillation, and chat serving. ## Featured ### Qwen3.6-27B is now available Train and serve Qwen3.6-27B on Freesolo. ## Featured ### 32k context for training and serving Train and serve supported models with context windows up to 32k tokens. # Explore the directory Source: https://docs.freesolo.co/directory-structure Every file flash env setup creates, what it's for, and when Flash reads it. `flash env setup` scaffolds a starter project into the **current directory**. A run is fully described by what lands on disk: an **environment** (your task and how it's scored) and a [**config**](/reference/configuration) (how to train on it). Every file is plain text you can read, diff, and version-control. Rerunning is safe: existing files stay untouched. ``` ./ ├── environment.py # the task + reward (a Freesolo environment) ├── evaluations.py # held-out eval suites for flash env eval ├── dataset/ │ └── train.jsonl # a tiny starter dataset (input/output rows) ├── configs/ │ ├── sft.toml # an SFT (supervised) training config │ ├── rl.toml # a GRPO (RL) training config │ └── opd.toml # an OPD (distillation) training config └── TRAINING.md # a playbook for the AI agent driving your runs ``` ### environment.py **What it is:** your [environment](/environment-model), the single source of truth for what the model practices on and how it's graded. It defines `load_environment()`, which returns a Freesolo `EnvironmentSingleTurn` (or `EnvironmentMultiTurn`) carrying a dataset and a `score_response` reward. This is the file you edit first. **When Flash reads it:** `flash env push` packages and uploads it, and every real training run imports it and calls `load_environment(**params)`. SFT `--cost`, `--dry-run`, and submit preparation do not import it: the control plane reads the selected packaged JSON or JSONL dataset directly. GRPO and OPD `--cost` quote offline from the catalog. The scaffolded starter loads its rows from `dataset/train.jsonl`. See the full scaffolded file — `StarterEnv` with `build_prompt_messages` and `score_response` — in [Environments](/guides/environments/overview#scaffold-one). ### evaluations.py **What it is:** held-out evaluation suites for the environment, defined as `BaseEvalSuite` subclasses returning `EvalCase` rows. It is optional: delete it and everything else still works. Keeping it gives you a fixed set of cases to score a trained model against, separate from the reward the model trains on. **When Flash reads it:** [`flash env eval`](/reference/cli#environments) scores the suites against a deployed model, and `flash env test` validates them offline without calling one. `flash env push` ships it alongside `environment.py`. ### dataset/train.jsonl **What it is:** a tiny starter dataset of `input`/`output` rows that the scaffolded `environment.py` loads. Replace it with your training rows before a real run. See [Datasets](/guides/datasets). **When Flash reads it:** your `environment.py` reads it during real training. For SFT estimates, the control plane reads the selected packaged JSON or JSONL file directly and tokenizes raw `input` and `output` fields plus the static training contract. It does not execute environment-defined prompt construction, filtering, or transforms. `flash env push` uploads the `dataset/` folder. ```jsonl dataset/train.jsonl theme={null} {"input":"What is 2 + 2?","output":"4"} {"input":"What is 3 + 5?","output":"8"} ``` ### configs/sft.toml **What it is:** an [SFT training config](/guides/training#choose-a-training-algorithm) for supervised fine-tuning on the `input`/`output` pairs in your environment's dataset. You set `model`, the `[environment] id`, and the `[train]` knobs (epochs, lora\_rank); the training infrastructure and artifact storage are [managed for you](/reference/configuration). Copy it per experiment. **When Flash reads it:** every `flash train`, `--dry-run`, and `--cost` parses this file. Dry-run sends the spec to the authenticated server for submit-time preflights. `--cost` stays local for GRPO and OPD. SFT `--cost` authenticates and returns a synchronous packaged-dataset estimate without starting paid training or allocating a training GPU. ```toml configs/sft.toml theme={null} project = "" # project uuid from `flash projects list` model = "Qwen/Qwen3.5-4B" algorithm = "sft" [environment] id = "" # paste the id returned by `flash env push --project --name my-env .` [train] epochs = 3 max_examples = 1000 lora_rank = 32 ``` ### configs/rl.toml and configs/opd.toml **What they are:** the same config shape with `algorithm = "grpo"` or `"opd"`. GRPO optimizes against your environment's `score_response` reward; OPD has a managed teacher grade your model's own completions, using `epochs` over the retained prompt pool with the step count derived for you, and warm-starts best from a finished SFT run via `init_from_adapter`. See [Choose a training algorithm](/guides/training#choose-a-training-algorithm). **When Flash reads them:** same as `sft.toml`. Keep the configs you need and pick one at train time. ```bash theme={null} flash train configs/rl.toml flash train configs/opd.toml ``` ### TRAINING.md **What it is:** a playbook for the AI coding agent you point at this project, including the OPD teacher preflight, rollout reasoning budgets, reward design, run interpretation, and common Flash issue mitigations. **When it travels:** if you publish the whole scaffolded folder, `flash env push` includes `.md` sidecars, so `TRAINING.md` can travel with the environment source in the Hub for humans and coding agents. The scaffolded `environment.py` is enough to publish on its own. Once your task grows data files or helper modules, move it into a folder with `environment.py` at the root and publish the whole folder. See [Structure the package](/guides/environments/package#structure-the-package). ## Next steps Fill in the environment class the scaffold starts you with. Point a config at your environment and submit a run. # Environment model Source: https://docs.freesolo.co/environment-model Your task as code: the dataset, interaction, and reward that make up the one part of a run you write. An **environment** is a small Python module that packages everything Flash needs to teach and grade your model: the data it practices on, how it interacts, and how its answers are scored. It is the single source of truth for *what the model learns* and *what counts as good*. ## What an environment packages Behind one `load_environment()` entrypoint it bundles a **dataset** (the prompts your model practices on, as [`input`/`output` records](/guides/datasets)), an **interaction model** (the class you subclass: one response, or a multi-turn exchange), and a **reward** (`score_response`, returning a `RewardResult`). That one environment drives **SFT** (learn from gold answers), **GRPO** (learn from reward scores), **OPD** (practice on the prompts while a managed teacher scores the model's tokens), and **eval**. Swap one line in the config; the environment stays put. For SFT, each row's `output` is the gold completion, appended after the environment's initial episode so system prompts and tool transcripts stay part of the example (see [Datasets](/guides/datasets#message-shaped-sft-targets) for the `output` shapes). You write that file; Flash owns everything else, from the training loop and managed compute to checkpointing, the versioned Environments Hub, and [per-token serving](/guides/deploy-and-chat#billing). The quality of your dataset and reward sets the ceiling on what training can achieve. The environment supplies the prompts for every algorithm, and the score for GRPO and eval; see [How Flash works](/how-flash-works#the-training-loop) for the algorithm side. ## Single-turn and multi-turn The base class you subclass sets the interaction model: * **`EnvironmentSingleTurn`:** prompt in, completion out, reward computed. Most tasks start here. If your prompt messages do not include a `system` message, the SDK prepends the run's prompt text as one. * **`EnvironmentMultiTurn`:** for conversations or tool use, where the model takes several steps before the whole sequence (its trajectory) is scored. You implement the episode hooks — `start_episode` (opening messages), `step_episode` (react to each action and decide whether the episode continues), `max_episode_turns` (the bound), and `score_episode` (reward the trajectory). See [Multi-turn environments](/guides/environments/multi-turn) for the full loop, an action protocol, and the stateless-step pattern. See [Environments](/guides/environments/overview) for the full SDK: prompt builders, loading dataset files, parameters, and secrets. With `thinking = true`, `response_text` is the answer text by default. It also exposes the separated reasoning trace and raw output when a reward needs them (see [Environments](/guides/environments/single-turn)). ## From local file to managed run You author an environment locally, but training runs on managed infrastructure, so it must be reachable by id: `flash env push` uploads the folder and prints an id, you put that id in `[environment] id`, and Flash imports it at run time. See [Package & publish](/guides/environments/package#publish-it). ## Next The full SDK: author a dataset and reward, then publish it. Package task records and data files inside an environment. # Examples Source: https://docs.freesolo.co/examples Eight end-to-end Flash training examples across SFT, GRPO, and OPD. The [flash-example repository](https://github.com/freesolo-co/flash-example) contains eight small, self-contained workflows across pure SFT, single-stage OPD, SFT-to-GRPO warm starts, and SFT-to-OPD warm starts. Each example packages its environment, reward-verified training rows, frozen 50-case held-out set, training config, and evaluation entrypoint. The results are task-specific evidence, not a general model ranking. Several students reach parity or near parity with GPT-5.5 under their exact boxed-answer, tool-use, or multi-turn contracts, and some score differences reflect strict protocol compliance rather than broad capability superiority. The numerical source of truth, including run ids, corrected-result history, footprint notes, and SFT-versus-RL ablations, is [`RESULTS.md`](https://github.com/freesolo-co/flash-example/blob/main/RESULTS.md). Browse the environments, training configs, frozen evaluations, and the task-specific numerical record in RESULTS.md. # Datasets Source: https://docs.freesolo.co/guides/datasets Package task records and data files with Freesolo environments. Datasets live inside the [environment](/environment-model). A [Flash config](/reference/configuration) points at one published environment id. The environment's `load_environment()` function returns the dataset, prompt builder, and reward logic that Flash uses for [SFT, GRPO, and OPD](/guides/training#choose-a-training-algorithm), and local validation. ```toml theme={null} [environment] id = "your-org/your-project/your-env" ``` ## Task records Author dataset rows with `input` and `output`: | Dataset key | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------------------- | | `input` | Prompt text for the model. | | `output` | Target answer or gold completion. For SFT this can be a scalar answer, `{ "messages": [...] }`, or a bare list of chat messages. | | `metadata` | Optional dict preserved on `example.metadata` and available to scoring. | The algorithm decides which column the model learns from. **SFT** trains directly on `output`, the gold answer. **GRPO** and **OPD** use only `input`: the model generates its own answers from the prompt, and learns from your reward (GRPO) or a teacher's token-level grading (OPD). `output` is optional for both. GRPO reads it only if your `score_response` uses it as a reference; OPD ignores it entirely, since the teacher, not your reward, is the training signal. `load_task_examples(...)` accepts a local file path or an iterable of records. * **File formats:** `.jsonl`, `.json`, `.csv`, `.txt`, or `.bson`. * **Field mapping:** `input` -> `example.input`, `output` -> `example.output`, `metadata` -> `example.metadata`. * **Original row:** the untouched record stays available as `example.record`. ```jsonl dataset/train.jsonl theme={null} {"input":"What is 2 + 2?","output":"4"} {"input":"What is 3 + 5?","output":"8"} ``` Each row must be `input` plus an optional `output`; alternate prompt or target key names are not accepted. Records are canonicalized to exactly `input`/`output`/`metadata`. You do not have to author these rows by hand. If your app already calls an LLM, [record its traffic as traces](/guides/tracing) and run `flash traces export`: every recorded call becomes one `input`/`output` row, written to `dataset/train.jsonl` by default. `flash env setup` can also pull a project's traces in while it scaffolds, so a new environment starts with your own data instead of the starter rows. When Flash builds your training records it keeps only `input`/`output`/`metadata` and **silently drops** every other top-level key before the row reaches training. Anything your scorer needs beyond the gold `output` string (a puzzle's `initial_board`, the `oracle_ids` a retrieval must return, unit tests to check code against, a grading rubric) has to live under `metadata`, or it is gone with no runtime warning. The one exception is the top-level `image`/`images` fields, which the multimodal loader reads from the original row (see [Image inputs](#image-inputs)). ### Message-shaped SFT targets For SFT, `output` is the gold completion appended after the environment's prompt messages. A scalar output becomes one assistant message. To teach a multi-turn trajectory or native tool calling, set `output` to `{"messages": [...]}` or a bare list of chat messages. Flash preserves those assistant, tool-call, tool-result, and reply messages when it builds the SFT example. ```jsonl dataset/train.jsonl theme={null} {"input":"Refund my last order.","output":{"messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_refund","type":"function","function":{"name":"refund_order","arguments":"{\"order\":\"last\"}"}}]},{"role":"tool","tool_call_id":"call_refund","content":"{\"ok\":true}"},{"role":"assistant","content":"Your last order has been refunded."}]}} ``` A message-shaped `output` is validated rather than coerced, so a malformed trajectory fails instead of collapsing into one bare turn: * `{"messages": ...}` must hold a **list**, and every entry in it must be an object. * `{"messages": [...]}` may not carry sibling keys - that shape is exactly one key. * A list mixing message objects with non-objects is rejected. A scalar `output` is still rendered into a single assistant message, so the strictness applies to targets that look like chat messages, not to plain answers. Use `Environment.sft_completion(example)` when your environment must synthesize or transform the gold completion before SFT. ### Multi-turn SFT masks observations For message-shaped multi-turn targets, Flash supervises authored assistant bodies and masks interleaved user, tool, and environment observations out of the loss. The model learns its replies without being trained to reproduce what the environment or tool returned. The run logs how many rows used role-aware assistant-body masking. If a rendered transcript is not parseable as ChatML, Flash keeps the row with completion-only fallback and prints a warning: the completion remains supervised, but masking of interleaved observations is not proven. Treat that warning as a dataset or template issue to inspect rather than assuming the non-assistant turns were excluded. ### Validate thinking-model SFT targets SFT on a thinking model (`thinking = true`) expects each gold completion to literally contain a `...` block. Catch missing blocks locally before submitting a run: ```python theme={null} from freesolo.datasets import load_dataset, warn_missing_think_tags dataset = load_dataset("dataset/train.jsonl") missing = warn_missing_think_tags(dataset.examples) # UserWarning + list of offending ids ``` It returns the ids of offending examples and emits a `UserWarning` naming the first few. Unlabeled records (no `output`) are skipped. ## Image inputs Every [catalog model](/reference/models) is multimodal, so a prompt can include images and Flash trains and serves on them. Images belong in the **prompt**, not in the gold completion: an image inside an SFT `output` message is rejected. Build a multimodal prompt the OpenAI way: a user message whose `content` is a list of parts, mixing text with one or more image parts. Flash accepts `image`, `image_url`, and `input_image` parts, and each image source can be a `data:` URI, raw bytes, a PIL image, or a package-root-relative path under `dataset/` (e.g. `dataset/photo.jpg`). Remote `http(s)` URLs are not accepted; see [below](#remote-urls): ```python environment.py theme={null} import base64 from pathlib import Path from freesolo.environments import EnvironmentSingleTurn ROOT = Path(__file__).parent class VisionEnv(EnvironmentSingleTurn): def build_prompt_messages(self, example, prompt_text): raw = (ROOT / "dataset" / example.metadata["image"]).read_bytes() uri = "data:image/jpeg;base64," + base64.b64encode(raw).decode() return [ { "role": "user", "content": [ {"type": "text", "text": example.input}, {"type": "image_url", "image_url": {"url": uri}}, ], } ] ``` A task record can also carry a top-level `image` (single) or `images` (a list). These are the one exception to the field whitelist above: the multimodal loader reads them from the original row (`example.record`) and appends them to the first user message, so they survive even though they are not part of the canonical `input`/`output`/`metadata` mapping. ### Training image limits The limits apply per training example, across prompt content blocks and top-level `image` or `images` fields together: | Limit | Maximum | | --------------------------- | ----------: | | Image count | 8 | | One encoded or file source | 20 MiB | | All source images combined | 32 MiB | | Width | 8192 pixels | | Height | 8192 pixels | | Pixels in one image | 40 million | | All decoded images combined | 128 MiB | These are training limits. Serving has a separate limit of four images per request; see [Send images](/guides/deploy-and-chat#send-images). ### Remote URLs Remote `http(s)` image URLs are **not supported**, and there is no flag to enable them: Flash never fetches a user-supplied URL at training time, so the training input stays fixed at submit. Include the image in the environment package as a relative path, or embed it as a data URI. A relative path is resolved from the package root and must stay inside `dataset/` (e.g. `dataset/photo.jpg`). Algorithm boundaries: * **SFT:** Images may appear only in the prompt. An image in a gold `output` message is rejected; the target remains text or tool messages. * **GRPO:** Images from the initial prompt persist as conditioning media for every turn of a multi-turn rollout. A later image returned by `step_episode()` is rejected because dynamic per-turn media is not supported. * **OPD:** Image prompts require a vision-capable `teacher_model`, and image-bearing OPD is single-turn only. A text-only teacher or a multi-turn image environment is rejected rather than distilling without the intended pixels. See [image-bearing OPD](/reference/configuration#image-bearing-opd). Package image files under `dataset/` so they upload with the environment (see [What gets uploaded](#what-gets-uploaded)). ## Load sidecars Read packaged files relative to `__file__` so paths work locally and in a managed run. ```python environment.py theme={null} from pathlib import Path from freesolo.datasets.records import load_task_examples from freesolo.environments import EnvironmentSingleTurn ROOT = Path(__file__).parent class MathEnv(EnvironmentSingleTurn): def __init__(self, *, split: str = "train") -> None: # read a packaged dataset file relative to environment.py self.dataset = load_task_examples(ROOT / "dataset" / f"{split}.jsonl") ``` The rest of the env class (`build_prompt_messages`, `score_response`) is covered in [Environments](/guides/environments/single-turn). Then select the split from your Flash config: ```toml theme={null} [environment] id = "your-org/your-project/math" [environment.params] split = "eval" ``` `[environment.params]` values are passed to your `load_environment(**kwargs)`. `split` is also honored by Flash itself: for an environment packaged with dataset files, `split = "eval"` selects `dataset/eval.jsonl` (or `.json`) as the dataset Flash trains on: SFT targets and GRPO problem selection alike. If the environment packages a default train split but the requested split file does not exist, the run fails at load time instead of silently falling back to `train.jsonl`. An explicit `dataset_path` param takes precedence over `split`. If your `load_environment()` builds `self.dataset` in code, those rows are what Flash trains on - the packaged file is only the fallback for an environment that exposes no dataset of its own. Filtering, subsampling, augmentation, and deduplication done in Python are honored rather than overwritten by a re-read of the file. That holds even when your filter matches nothing: an empty in-code dataset fails rather than quietly falling back to the unfiltered file and training on the rows you meant to exclude. An explicitly selected `dataset_path`, `records`, or side split still wins over an in-code dataset. ## What gets uploaded For a local environment directory, `flash env push` includes: * `environment.py`, always at the artifact root. * Sibling Python helper files. * Sidecar directory named `dataset`. * Common sibling data files such as `.jsonl`, `.json`, `.csv`, `.txt`, `.md`, `.parquet`, `.tsv`, `.yaml`, and `.yml`. Workspace metadata, cache directories, virtualenvs, and version-control files are skipped, and **secrets are never uploaded**: `.env` files and files matching `*.key`, `*.pem`, `*.pfx`, `*.p12`, `credentials*`, or SSH key names (`id_rsa*`, `id_ed25519*`, and similar) are excluded. Keep the artifact small: environment uploads are capped at 64 MB compressed, 256 MB uncompressed, and 5000 files. For large corpora, keep the data in an external store and pass the identifier or URL through `[environment.params]`. When you push a single `.py` file (or a folder with just one top-level `.py`), only that entrypoint, a sibling `README.md`/`TRAINING.md`, and any `dataset/` tree are packaged, and sibling helper modules are not. An `evaluations.py` is not packaged in single-file mode either; push the folder if you want [`flash env eval`](/reference/cli#environments) suites to travel with the environment. ## Next steps Load these records inside an environment class. Train SFT, GRPO, or OPD on your dataset. # Deploy & chat Source: https://docs.freesolo.co/guides/deploy-and-chat Serve a trained adapter and talk to it over an OpenAI-compatible API. Deploy a trained adapter and chat with it from the CLI or any OpenAI-compatible client, once its [training run](/guides/training) reaches `done`. `flash models deploy` registers your adapter with Freesolo's **managed serving service**. Send requests to it and pay per token (see [Billing](#billing)). ## Deploy ```bash theme={null} flash models deploy ``` Preview what a deploy would do without creating it: ```bash theme={null} flash models deploy --dry-run ``` Every real deploy resolves the adapter to an immutable Hugging Face commit, runs a bounded serving smoke against that revision, and atomically activates the stable run-id alias only if the smoke passes; otherwise the existing alias is left unchanged. `flash models deploy` returns while the revision is still queued, so the model is not servable the moment the command exits. To gate a script on the deployment, add `--wait`: ```bash theme={null} flash models deploy --wait ``` It blocks until the revision is servable and exits `0`, or exits `1` if the deployment fails, rolls back, or the wait times out. The default timeout is 2400 seconds; pass `--wait 600` for your own budget, or `--wait 0` for a single state read that does not block. Interrupting with `Ctrl-C` stops the waiting, not the deployment. You can also deploy and undeploy from the model's detail page in the [dashboard](/platform#models). Both paths run the same verification and produce the same alias, so a deployment started in the web app is visible to `flash models deployments` and can be torn down with `flash models undeploy`. ## Deploy a specific checkpoint Every training step that saved an adapter is independently deployable. List a run's deployable checkpoints: ```bash theme={null} flash runs checkpoint ``` Then serve a specific step instead of the final adapter: ```bash theme={null} flash models deploy /step- ``` Deploy a checkpoint while a run is still training, or after a GRPO run stops with useful intermediate steps. Checkpoint deployments attach serving metadata to the run without altering its training state. A run that never finalized (cancelled or preempted mid-training) has no final adapter, so a plain `flash models deploy ` fails with an error that lists the saved checkpoint steps and the exact `flash models deploy /step-N` command to deploy one of them instead. ## Stable aliases and immutable revisions Final adapters use `RUN_ID@final.<40-char-sha>` and saved checkpoints use `RUN_ID@step-N.<40-char-sha>`. Each run has exactly one shared mutable bare `RUN_ID` alias. Deploying another checkpoint does not create a second bare alias; after the new revision passes its bounded smoke, it moves the shared alias for every caller using `RUN_ID`. The CLI warns before moving it away from a different checkpoint. Older immutable revisions can remain directly callable. `RUN_ID/step-N` is a deploy and export selector. Once you have deployed that checkpoint, `flash models chat RUN_ID/step-N` works from the CLI too, resolving to that checkpoint's verified revision without depending on where the bare alias currently points. From an OpenAI client, set `model` to the bare run alias or the full immutable revision from `flash models deployments`; the `RUN_ID/step-N` shorthand is a Flash-CLI selector, not an OpenAI model name. ## Billing Serving is **billed per token**. Prompt and completion tokens have per-model rates, and cached prompt tokens use the model's cached-input rate. See prices in [Supported models](/reference/models#serving-prices). **Cached prompt tokens are cheaper than uncached prompt tokens.** Prefix caching is always on: when a request's prompt shares a leading prefix with a recent one — a shared system prompt, or the growing context of a multi-turn chat — the serving reuses that prefix from cache instead of recomputing it. Only the input prefix can be cached, so completion tokens always bill at the full per-token rate. When a request hits the prefix cache, the response reports how many prompt tokens were served from cache in `usage.prompt_tokens_details.cached_tokens`. Sub-cent usage is carried forward, so small requests are accounted for without rounding each one up to a cent. ## Chat from the CLI ```bash theme={null} flash models chat -m "Summarize the plot of Hamlet in two sentences." ``` `flash models chat` accepts the stable run alias, a full immutable revision, or `RUN_ID/step-N` for a checkpoint you have already deployed. A `RUN_ID/step-N` target resolves to that checkpoint's verified revision, so deploy it first with `flash models deploy RUN_ID/step-N`. `-m`/`--message` is required; `--system`, `--max-tokens` (default 512), and `--temperature` (default 0.0) are optional. See the [CLI reference](/reference/cli#serving) for the full flag list. ```bash theme={null} flash models chat -m "Write a haiku about mountain weather" --temperature 0.8 --max-tokens 128 ``` `--system` is transient — it applies to that one request and is not stored with the deployment. Use it to probe the adapter with the same system prompt it was trained with: ```bash theme={null} flash models chat -m "What is 6*7?" --system "Answer with just the number." ``` ## Manage deployments ```bash theme={null} flash models deployments # human-readable deployment status flash models deployments --json # complete machine-readable records flash models undeploy # disable the alias and immutable revisions ``` Human output shows run id, step, revision, state, verification time, OpenAI model, and detail. JSON output includes `openai_base_url` for clients. ## Use it from your own code ### Run-scoped chat route For a deployed run you own, post directly to the Flash control plane. The route checks run ownership, resolves the active verified deployment, and preserves the run's serving options such as trained thinking mode: ```bash theme={null} curl -X POST "https://flash.freesolo.co/v1/runs//chat" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"messages":[{"role":"user","content":"Hello!"}],"max_tokens":256}' ``` The response uses the OpenAI chat-completions shape. Omit selectors to use the shared bare run alias, or pass exactly one of `"step": N` and `"adapter_revision": ""` to pin a checkpoint that has already passed deployment verification for that run. If the run has no ready deployment, the route returns a conflict with a deploy hint. ### Direct OpenAI-compatible endpoint The direct serving endpoint remains available. Use `flash models deployments --json` and pass its `openai_base_url` as the SDK base URL. Set `model` to the shared bare run alias or a full immutable revision. A valid Freesolo API key is required, and the key's org must own the adapter named in `model`: ```python theme={null} from openai import OpenAI client = OpenAI( base_url="https://serve.freesolo.co/v1", # openai_base_url from flash models deployments --json api_key="", # use the same freesolo key as flash login ) resp = client.chat.completions.create( model="", messages=[{"role": "user", "content": "Hello!"}], ) print(resp.choices[0].message.content) ``` For deployed adapters, serving preserves the run's trained thinking mode. A caller cannot override `enable_thinking` per request for an adapter. ### Reasoning is returned separately When a generation runs in thinking mode, the reasoning comes back in `reasoning_content` and only the answer in `content`. The `` separator is not included in either field: ```python theme={null} resp = client.chat.completions.create( model="", messages=[{"role": "user", "content": "Hello!"}], ) print(resp.choices[0].message.reasoning_content) # how it got there print(resp.choices[0].message.content) # the answer ``` Streaming splits the same way: reasoning tokens arrive as `choices[0].delta.reasoning_content` and answer tokens as `choices[0].delta.content`, so accumulate both. If a generation stops before the reasoning closes, everything generated is returned as `reasoning_content` and `content` is empty. Non-thinking completions are unaffected and use `content` alone. `flash models chat` folds both fields into one balanced `...` block; only direct endpoint calls expose the split shape. ## Send images Every catalog model accepts **image inputs**, so a served adapter or base model can take multimodal prompts over the same OpenAI-compatible API. Pass images as OpenAI `image_url` content parts. Serving accepts base64 `data:` URIs only; remote (`http(s)`) and `file:` URLs are rejected. A single request accepts up to four images: ```python theme={null} import base64 with open("photo.jpg", "rb") as f: data_uri = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode() resp = client.chat.completions.create( model="", messages=[ { "role": "user", "content": [ {"type": "text", "text": "What is in this image?"}, {"type": "image_url", "image_url": {"url": data_uri}}, ], } ], ) ``` To train on image data, see [Image inputs](/guides/datasets#image-inputs). ## Structured outputs Constrain a response to valid JSON with the OpenAI-standard `response_format`. A training `structured_outputs` constraint becomes the deployed adapter's default. A request-level `response_format` can override it with `text`, `json_object`, or `json_schema`. This works on adapters and base models: ```python theme={null} resp = client.chat.completions.create( model="", messages=[{"role": "user", "content": "Give me a person as JSON."}], response_format={ "type": "json_schema", "json_schema": { "schema": { "type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, "required": ["name", "age"], } }, }, ) ``` `{"type": "json_object"}` forces any valid JSON with no fixed schema, and `{"type": "text"}` leaves output unconstrained. With thinking enabled, reasoning is free-form and the grammar begins after ``. Real deployments fail before alias activation if structured output is invalid, unclosed, or truncated. See the [Structured outputs](/guides/structured-outputs) guide to also constrain training rollouts. ## Chat with a base model (no adapter) Using the same OpenAI-compatible client, set `model` to any id from `flash models list`, such as `Qwen/Qwen3.5-4B`, to generate against the base weights with no LoRA and no `flash models deploy`: ```python theme={null} resp = client.chat.completions.create( model="Qwen/Qwen3.5-4B", # a base-model id, not a run id — no deploy needed messages=[{"role": "user", "content": "Hello!"}], ) ``` Any valid Freesolo API key reaches a base model — there is no adapter to own — and the tokens are **billed to your own org** at that model's [serving prices](/reference/models#serving-prices). Unlike a deployed adapter, which serves the reasoning behavior it was trained with, a base model honors the `enable_thinking` you pass in `chat_template_kwargs`. ```python theme={null} resp = client.chat.completions.create( model="Qwen/Qwen3.5-4B", messages=[{"role": "user", "content": "Answer directly: 17 * 19"}], extra_body={"chat_template_kwargs": {"enable_thinking": False}}, ) ``` ## Serve in your own account Everything above runs on Freesolo's managed serving, billed per token. You can instead run the same serving stack in **your own Modal or RunPod account** with `flash serve deploy`. The GPU runs and bills in your account, and you call the provider's HTTPS endpoint directly - no Freesolo gateway in the request path. This needs the `server` extra: `pip install 'freesolo-flash[server]'`. One command provisions one deployment: one base model, one run's adapter. ```bash theme={null} export HF_TOKEN=hf_... # lets the container fetch your adapter export FLASH_SERVING_KEY=$(python -c 'import secrets; print(secrets.token_urlsafe(32))') export MODAL_TOKEN_ID=... MODAL_TOKEN_SECRET=... # `modal token new` writes these flash serve deploy \ --provider modal \ --model Qwen/Qwen3.5-4B \ --run \ --deployment-id my-4b-serving \ --image ghcr.io/freesolo-co/freesolo-flash-serve@sha256: \ --artifact-repo \ --artifact-subfolder \ --lora-rank 32 \ --modal-workspace \ --modal-environment main \ --modal-region us-east ``` For RunPod, export `RUNPOD_API_KEY` and **swap** the placement flags rather than adding to them - `--provider runpod` takes `--runpod-account` and `--runpod-data-center`, and rejects the `--modal-*` flags instead of ignoring them: ```bash theme={null} flash serve deploy \ --provider runpod \ --model Qwen/Qwen3.5-4B \ --run \ --deployment-id my-4b-serving \ --image ghcr.io/freesolo-co/freesolo-flash-serve@sha256: \ --artifact-repo \ --artifact-subfolder \ --lora-rank 32 \ --runpod-account \ --runpod-data-center ``` `--image` must be pinned to a digest (`name@sha256:...`), never a tag, so the deployment cannot drift to different content after it is recorded. Add `--dry-run` to resolve and validate every input - including the adapter's provenance - without contacting the provider or spending anything. ### Call your endpoint `deploy` prints the endpoint URL. Authenticate with the `FLASH_SERVING_KEY` you generated, which is scoped to that one deployment: ```bash theme={null} curl https:///v1/chat/completions \ -H "Authorization: Bearer $FLASH_SERVING_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "", "messages": [{"role": "user", "content": "hello"}]}' ``` The deployment serves `/healthz`, `/v1/models`, and `/v1/chat/completions`. It receives its adapters in an immutable manifest at boot, so `flash models deploy` cannot drive it and never should: those commands carry `FREESOLO_INTERNAL_KEY`, and a customer-owned endpoint does not read that header at all. ### Check and tear down `flash serve status` proves the current state without changing anything, and `flash serve undeploy` removes one generation and proves its resources are gone. Both take the same identity flags as `deploy`, or the `identity` string that `deploy` printed via `--deployment-identity`. Pass the provider resource ids `deploy` printed to `undeploy` so each deletion binds to the exact generation. Provider credentials are read for **one call and never stored**, so export them again for each command. If `deploy` ends in `provisioning` or `outcome_unknown`, resources may be live and billing in your account. Run `flash serve status` to inspect, then `flash serve undeploy` to stop them. Do not re-run `deploy` - that provisions and bills a second time. ## Export to your own HuggingFace repo Copy a trained adapter out of Freesolo's managed storage into a repo you own: ```bash theme={null} flash models export --adapter-id --repository / ``` `--adapter-id` and `--repository` are required; `--api-key` (defaults to `HF_TOKEN`) and `--public` are optional. See the [CLI reference](/reference/cli#export) for the full flag list. ## Next steps Per-token rates for every model. Every deploy, chat, and serving command. # Multi-turn environments Source: https://docs.freesolo.co/guides/environments/multi-turn Conversations, tool use, and agents with EnvironmentMultiTurn. Use `EnvironmentMultiTurn` for tasks that take more than one response: a conversation, a tool-using agent, anything where the environment reacts to what the model just did and the model acts again. Subclass it and implement the episode hooks. GRPO rolls out the whole episode, then scores the finished trajectory. OPD can use the same plain multi-turn loop and distills the student's assistant turns from the managed teacher; native tool-calling environments remain GRPO-only. | Single-turn (`EnvironmentSingleTurn`) | Multi-turn (`EnvironmentMultiTurn`) | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `build_prompt_messages` → one prompt | `start_episode` → opening messages | | `score_response` → reward one reply | `step_episode` → react to each action; `max_episode_turns` → bound the episode; `score_episode` / `score_episodes` → reward trajectories | `flash env setup --multi-turn` scaffolds a runnable starter on this class — a "guess the secret number" episode with `start_episode`, `step_episode`, `max_episode_turns`, and `score_episode` wired end-to-end — that you edit into your own task. ## The episode loop During training the trainer drives your hooks in a loop, then scores the result: ```text theme={null} messages = env.start_episode(example, prompt_text) # opening system/user messages response_text = "" for _ in range(env.max_episode_turns(example)): action = policy.sample(messages) # one assistant message messages.append({"role": "assistant", "content": action}) result = env.step_episode(example, messages, action) if result.final_response_text is not None: response_text = result.final_response_text else: response_text = action messages.extend(result.messages) # the env's response (tool result, next user turn) if result.done: break episode = EnvironmentEpisode(messages=tuple(messages), response_text=response_text) reward = env.score_episode(example, episode) # Flash batches completed rollouts by calling env.score_episodes(example, episodes). ``` * **`start_episode(example, prompt_text)`** returns the initial messages (e.g. a system prompt plus the first user message). * **`max_episode_turns(example)`** caps the assistant actions Flash may sample before forcing the episode to end, per example. * **`step_episode(example, messages, assistant_response)`** is the core. `messages` already includes the assistant action being stepped as its last element. Return an `EnvironmentStepResult`: * `done=False, messages=(...)` keeps the episode open and injects the environment's response (a tool result, the next user turn); * `done=True, final_response_text=...` ends it. `final_response_text` overrides the last assistant message as the text passed to scoring. * **`score_episode(example, episode)`** grades one completed transcript (`episode.messages`) and returns a `RewardResult`, exactly like `score_response` but over the whole trajectory. * **`score_episodes(example, episodes)`** is the batch wrapper Flash calls for a GRPO group. You normally do not implement it; the SDK default runs your singular `score_episode` for each item, using `max_score_concurrency`. ## Full multi-turn contract ```python theme={null} from dataclasses import dataclass, field from typing import Any from freesolo.datasets import TaskExample from freesolo.environments import RewardResult ChatMessage = dict[str, Any] class EnvironmentMultiTurn: def start_episode( self, example: TaskExample, prompt_text: str, ) -> list[ChatMessage]: ... def max_episode_turns(self, example: TaskExample) -> int: ... def step_episode( self, example: TaskExample, messages: list[ChatMessage], assistant_response: str, ) -> "EnvironmentStepResult": ... def score_episode( self, example: TaskExample, episode: "EnvironmentEpisode", ) -> RewardResult: ... def score_episodes( self, example: TaskExample, episodes: list["EnvironmentEpisode"], ) -> list[RewardResult]: ... @dataclass(frozen=True) class EnvironmentStepResult: done: bool = True messages: tuple[ChatMessage, ...] = () final_response_text: str | None = None metadata: dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) class EnvironmentTurn: role: str content: str name: str | None = None metadata: dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) class EnvironmentEpisode: messages: tuple[ChatMessage, ...] response_text: str turns: tuple[EnvironmentTurn, ...] = () metadata: dict[str, Any] = field(default_factory=dict) latency_ms: int | None = None total_tokens: int | None = None ``` `EnvironmentStepResult.messages` should contain only new environment-side messages to append after the assistant action. Do not repeat the assistant message Flash just sampled. `metadata` is appended to the completed episode's metadata under `steps`, available to `score_episode` and logs. Keep durable episode state in the transcript, not in hidden mutable attributes. ## An action protocol the model can emit as text The trained policy is an open model, not a tool-use API, so define a plain-text protocol it can produce and your `step_episode` can parse — for example, a tool-call block versus a final reply: ```python environment.py theme={null} import json, re from freesolo.datasets import TaskExample from freesolo.environments import ( EnvironmentEpisode, EnvironmentMultiTurn, EnvironmentStepResult, RewardResult, ) TOOL_CALL = re.compile(r"\s*(.*?)\s*", re.DOTALL) def run_tool(name: str, args: dict) -> dict: ... # your task-specific tool or service class AgentEnv(EnvironmentMultiTurn): def __init__(self, *, split: str = "train") -> None: self.dataset = [{"input": "Refund my last order.", "output": "refund_order"}] def start_episode(self, example: TaskExample, prompt_text: str): rules = ( "Call tools with {...}; " "reply in plain text when done." ) return [ {"role": "system", "content": f"{prompt_text}\n\n{rules}".strip()}, {"role": "user", "content": example.input}, ] def max_episode_turns(self, example: TaskExample) -> int: return 8 def step_episode(self, example, messages, assistant_response): blocks = TOOL_CALL.findall(assistant_response) if blocks: # a TOOL turn: run the calls, feed results back, stay open results = [run_tool(**json.loads(b)) for b in blocks] return EnvironmentStepResult( done=False, messages=({"role": "user", "content": f"{json.dumps(results)}"},), ) # a REPLY turn: no tool call -> the model is done return EnvironmentStepResult(done=True, final_response_text=assistant_response) def score_episode(self, example: TaskExample, episode: EnvironmentEpisode) -> RewardResult: used = [json.loads(b)["name"] for m in episode.messages for b in TOOL_CALL.findall(m["content"]) if m["role"] == "assistant"] hit = str(example.output) in used return RewardResult(score=1.0 if hit else 0.0, threshold=1.0) def load_environment(split: str = "train", **kwargs) -> AgentEnv: return AgentEnv(split=split) ``` ## SFT targets for multi-turn SFT does not execute the multi-turn loop. It builds one supervised row as: ```text theme={null} prompt_messages = env.start_episode(example, prompt_text) target_messages = env.sft_completion(example) training_text = chat_template(prompt_messages + target_messages) ``` The default `sft_completion(example)` turns `example.output` into the target messages (see [Datasets](/guides/datasets#message-shaped-sft-targets)). When ChatML role parsing succeeds, Flash supervises authored assistant bodies and masks non-assistant target turns. An unparseable transcript keeps completion-only fallback and logs a warning that observation masking is unproven. See [Multi-turn SFT masking](/guides/datasets#multi-turn-sft-masks-observations). A scalar target remains one assistant turn. SFT does not run `step_episode`; provide a target transcript or override `sft_completion` for multi-turn SFT. ## Keep `step_episode` stateless The SDK doesn't hand you a per-episode scratch object, and GRPO runs **many rollouts of the same example concurrently** — so don't stash mutable world-state on the env keyed by example. If your task has state (an API or service that changes as tools run), make the **transcript the source of truth** and rebuild state by replaying `messages` inside `step_episode` / `score_episode`. Preserve these three replay invariants: 1. `messages[-1]` is the same current action as `assistant_response`. Replay `messages[:-1]`, then apply the current action once. ```python theme={null} state = initial_state(example) for action in prior_sampled_actions(messages[:-1]): state = reduce_action(state, action) state = reduce_action(state, assistant_response) ``` 2. Role alone is insufficient when the prompt contains assistant demonstrations or the environment appends assistant-role replies. Mark or otherwise distinguish sampled actions. 3. Historical replay must be pure. Perform external effects once for the current action, and make retries idempotent. ## Score behaviours, not a script Open-ended tasks rarely have one correct transcript, so don't grade by imitation. Score the behaviours you care about — did it take the resolving action, follow required ordering (e.g. authenticate before a privileged call), stay within limits — and return that blend from `score_episode`. Keep the reward in a plain importable function so it stays unit-testable. ## Per-turn credit (GRPO) By default, multi-turn GRPO assigns **one reward to the whole episode** (`credit_assignment = "per_episode"`): every assistant turn shares the trajectory's group-relative advantage. For a pure multi-turn task where each turn is independently gradable, set `credit_assignment = "per_turn"` in `[train]` to give **each assistant turn its own group-relative advantage** instead. Supply the per-turn signal from `score_episode` by returning a `RewardResult` whose `metadata` carries a `per_turn_rewards` list, one finite value per recorded sampled assistant turn, in order. `episode.turns` excludes the seeded `start_episode` prompt but includes environment replies. When environment replies use only user or tool roles, filter to assistant turns: ```python theme={null} def score_episode(self, example, episode) -> RewardResult: # safe when environment replies never use the assistant role turns = [t for t in episode.turns if t.role == "assistant"] per_turn = [self.grade_turn(t) for t in turns] return RewardResult( score=sum(per_turn) / len(per_turn) if per_turn else 0.0, metadata={"per_turn_rewards": per_turn}, ) ``` If your environment appends assistant-role replies, mark or otherwise distinguish them so they are not graded as sampled actions. Include every recorded sampled assistant turn even when your action parser rejects its content. `per_turn_rewards` must contain one finite ordered value per recorded sampled assistant turn. A missing or invalid vector makes the affected rollout group use episode-level credit and logs a warning. `per_turn` credit is rejected for native tool-calling multi-turn environments, and a single-turn run is identical either way. ## Configure for multi-turn For multi-turn runs, the episode budget comes from `max_episode_turns`. What matters in `[train]`: * **`max_completion_tokens`** is *per assistant action* (one tool call or one reply), not per episode — keep it modest. * **`max_context_tokens`** must hold the system prompt **plus the whole running transcript** (every turn, every tool result), so set it well above a single-turn task. * **`group_size`** is the number of full episodes rolled out per prompt for the GRPO advantage estimate. OPD usually uses `group_size = 1`. ### Managed generation request deadlines Pure multi-turn GRPO model generations have managed request deadlines between 10 and 60 minutes, based on context size. A timed-out assistant generation is aborted and retried once; timed-out output is never sent to the environment. This covers only model generation in the pure multi-turn GRPO path. It does not cover environment or external-service calls, OPD, or tool-loop paths. ## Next steps Submit a run once your environment is ready. Serve the trained adapter. # Environments Source: https://docs.freesolo.co/guides/environments/overview What a Freesolo environment is, and how to scaffold your first one. An [**environment**](/environment-model) is the task: the data your model sees and the reward, a numeric score your code assigns to each model output. Flash loads environments through the Freesolo environment SDK. Author one locally, publish it to Freesolo's managed Environments Hub, and reference it in config by its Freesolo environment id. ```toml theme={null} [environment] id = "your-org/your-project/your-env" ``` ## Scaffold one ```bash theme={null} flash env setup --project ``` Setup needs a project: run `flash login` first, then pass `--project` (from `flash projects list`) or let an interactive run prompt you to choose one. The UUID is written into every generated config. This scaffolds a starter project in the current directory: `environment.py`, a tiny `dataset/train.jsonl`, three configs (`configs/sft.toml` for SFT, `configs/rl.toml` for GRPO, and `configs/opd.toml` for OPD), and a `TRAINING.md` playbook for the coding agent you point at the project. The starter `load_environment()` returns a Freesolo `EnvironmentSingleTurn` with a sample dataset and reward. On an interactive terminal, `flash env setup` first runs a short survey: whether the model handles your task in a single turn or a bounded multi-turn episode, and whether to train with reasoning. Answer up front with `--single-turn` / `--multi-turn` and `--reasoning` / `--no-reasoning`, or pass `-y` to take the defaults (single-turn, no reasoning); non-interactive runs (no TTY or CI) always take the defaults. `--multi-turn` scaffolds a runnable `EnvironmentMultiTurn` starter with the episode hooks wired end-to-end instead of the single-turn class, and `--reasoning` sets `thinking = true` in the generated configs. ```python environment.py theme={null} import json from pathlib import Path from freesolo.datasets import TaskExample from freesolo.environments import EnvironmentSingleTurn, RewardResult DEFAULT_DATASET_PATH = Path(__file__).parent / "dataset" / "train.jsonl" def load_jsonl(path): with Path(path).open() as f: return [json.loads(line) for line in f if line.strip()] class StarterEnv(EnvironmentSingleTurn): dataset = load_jsonl(DEFAULT_DATASET_PATH) def build_prompt_messages(self, example: TaskExample, prompt_text: str): return [{"role": "user", "content": example.input}] def score_response(self, example: TaskExample, response_text: str) -> RewardResult: expected = str(example.output or "").strip() score = 1.0 if expected and expected in response_text else 0.0 return RewardResult(score=score, threshold=1.0) def load_environment(dataset_path: str | None = None, **kwargs) -> StarterEnv: env = StarterEnv() if dataset_path: env.dataset = load_jsonl(dataset_path) return env ``` Replace the dataset and reward with your task: * [**Dataset**](/guides/datasets): the prompts (and any gold answers) your model trains and is evaluated on. * **Reward**: `score_response` returns a `RewardResult`. GRPO optimizes that score; SFT trains directly on your dataset answers instead; OPD distills a teacher's token-level grading of the model's own completions. See [how Flash works](/how-flash-works). For multi-turn tasks (conversations, tool use, agents), use `EnvironmentMultiTurn` and implement the episode hooks. See [Multi-turn environments](/guides/environments/multi-turn) below. ## Use the SDK Your environment code imports from the `freesolo` package, and **managed runs already have it**. Install it locally only to run or test `environment.py` yourself: ```bash theme={null} uv venv source .venv/bin/activate uv pip install freesolo ``` ## Next steps Structure the folder and push it to the Hub. Write the dataset, prompts, and reward. # Package & publish Source: https://docs.freesolo.co/guides/environments/package Lay out the environment folder, declare dependencies, and publish it to the managed Hub. ## Structure the package A published environment is a small folder. Flash packages it, imports its `environment.py` entrypoint at run time, and calls `load_environment(**params)`, which must return a Freesolo SDK environment. The folder can be named anything as long as it contains `environment.py` at its root. ```text theme={null} math/ environment.py # defines load_environment() helpers.py # optional Python helpers dataset/ train.jsonl # input/output records eval.jsonl ``` The directory is `dataset/`, singular. Flash probes `dataset/.jsonl` and `dataset/.json` and never reads a top-level `datasets/`. A package that uses the plural name fails at load time naming the layout, rather than training on a silently empty dataset. Rename the directory, or point `[environment.params] dataset_path` at the exact file. A single `.py` upload still works for tiny smoke tests, but push real environments as folders. Import from sibling files in the folder you publish, from the `freesolo` SDK, from the Python standard library, or from any third-party package you declare under `[environment] pip` (see [Dependencies are managed](#dependencies-are-managed)). ### Dependencies are managed The GPU worker installs one managed requirement set - the `freesolo` SDK and the training stack - for every environment. Anything your scorer needs on top of that is declared with `[environment] pip`, which is **appended** to the managed set rather than replacing it; see [Scorer dependencies](/reference/configuration#scorer-dependencies). Without that key, your `environment.py` and its sibling modules can import only from: * the `freesolo` SDK, * the Python standard library, * other files in the folder you publish. A `pyproject.toml`, `requirements.txt`, or lockfile beside `environment.py` can describe your local development setup, but Flash does not read those files to install packages for a managed run. If your reward logic needs an external service - a judge model, a database - declare its client under `[environment] pip`, or call the service over HTTP with the standard library if you would rather not add a dependency. Either way, declare the credential it reads under `[environment] secrets` (see [Use API keys](#use-api-keys)): ```python environment.py theme={null} import json import os import urllib.request JUDGE_API_KEY = os.environ["JUDGE_API_KEY"] def judge(prompt: str) -> float: body = json.dumps({"model": "some-judge", "messages": [{"role": "user", "content": prompt}]}) request = urllib.request.Request( "https://openrouter.ai/api/v1/chat/completions", data=body.encode(), headers={ "Authorization": f"Bearer {JUDGE_API_KEY}", "Content-Type": "application/json", }, ) with urllib.request.urlopen(request, timeout=30) as response: payload = json.load(response) return float(payload["choices"][0]["message"]["content"].strip()) ``` Give every outbound call an explicit timeout. A reward that blocks holds up the whole training step, and slow grading shows up as wall-clock cost. Do not declare unused secrets. If the active reward only calls the judge model and no longer queries a database, leave the database credential out. ## Publish it Training runs on managed infrastructure, so the environment must be reachable by id. Push the folder to the managed Environments Hub. Every environment belongs to a **project**, the same grouping the [dashboard](/platform) organizes runs and traces by, and you name it on every push. Get a project UUID from `flash projects list`, or create one: ```bash theme={null} PROJECT=$(flash projects create my-project) flash env push --project "$PROJECT" --name math math ``` The `--name` value is normalized to a lowercase hyphen slug, and the command prints the published id (`your-org/your-project/math`) to put in `[environment] id`. A malformed UUID is rejected before anything uploads, and a project outside your organization fails the push. See [`flash env push`](/reference/cli#environments). Environment names are unique **per project**, not per organization. The owning project is part of the id, so two projects in your organization can each publish their own `math` and they stay separate environments. Renaming a project does not change any id it already published. ## Use an existing environment If you already have a published env id (yours or one shared with you), reference it directly. On Freesolo's managed service, `[environment] id` accepts only a managed `namespace/project/name` slug; `github:` refs and GitHub URLs are rejected. Publish yours with `flash env push`, use the returned id, and note that `flash env pull` also accepts managed slugs only. Standalone planes use reachable GitHub sources instead; see [Self-hosted environments](/guides/self-hosting#environments). ```toml theme={null} [environment] id = "your-org/your-project/your-env" ``` To edit or inspect the source locally, pull the whole environment into a directory: ```bash theme={null} flash env pull your-org/your-project/your-env ``` Pull one file by adding its path inside the environment. Use `-o` to choose the destination and `-f` to overwrite an existing output: ```bash theme={null} flash env pull your-org/your-project/your-env dataset/train.jsonl -o train.jsonl ``` ## List what you have ```bash theme={null} flash env list ``` Shows local environment sources you can publish, such as `./environment.py` or folders under `environments/`, and your organization's already-published environments with the ids you paste into `[environment] id`. ## Delete a Hub environment Delete only targets managed Hub ids of the form `namespace/project/name`; GitHub refs and local paths are not Hub records and cannot be deleted this way. ```bash theme={null} flash env delete your-org/your-project/your-env --project flash env delete your-org/your-project/your-env --project -y # skip the confirmation prompt ``` `--project` is required and must be the UUID of the project the environment was published to. `flash projects list` shows the projects you can reach, and the environment's dashboard page shows the one it belongs to. ## Use API keys If your environment needs an external service, read the key from `os.environ` in `environment.py`: ```python environment.py theme={null} import os SERVICE_API_KEY = os.environ["SERVICE_API_KEY"] ``` Then declare the environment variable names in your training config: ```toml theme={null} [environment] id = "your-org/your-project/your-env" secrets = ["SERVICE_API_KEY"] ``` Set the value in your shell before submitting: ```bash theme={null} export SERVICE_API_KEY="..." flash train configs/sft.toml ``` You can also put local development values in `.env` or `.env.local`: ```bash theme={null} SERVICE_API_KEY=... ``` Declared secret values are sent separately from everything else, and are never stored in the TOML config, status JSON, logs, or the published environment. Do not put API keys in `[environment.params]`. If a declared secret is missing when you submit, `flash train` fails before the run starts. ## Next steps Fill in the environment class you just published. Reference the published id from a config and train. # Single-turn environments Source: https://docs.freesolo.co/guides/environments/single-turn Write an EnvironmentSingleTurn class: dataset, prompt builder, and reward. A single-turn environment resolves each example in one model response - prompt in, completion out, reward computed. Subclass `EnvironmentSingleTurn` and implement a prompt builder and a reward: ```python environment.py theme={null} from pathlib import Path from freesolo.datasets import TaskExample from freesolo.datasets.records import load_task_examples from freesolo.environments import EnvironmentSingleTurn, RewardResult ROOT = Path(__file__).parent class MathEnv(EnvironmentSingleTurn): def __init__(self, *, split: str = "train") -> None: self.dataset = load_task_examples(ROOT / "dataset" / f"{split}.jsonl") def build_prompt_messages(self, example: TaskExample, prompt_text: str): return [{"role": "user", "content": example.input}] def score_response(self, example: TaskExample, response_text: str) -> RewardResult: expected = str(example.output or "").strip() score = 1.0 if expected and expected in response_text else 0.0 return RewardResult(score=score, threshold=1.0) def load_environment(split: str = "train", **kwargs) -> MathEnv: return MathEnv(split=split) ``` The dataset file uses the `input`/`output` row shape ([Task records](/guides/datasets#task-records)). `load_task_examples(...)` exposes each record's `input`/`output` as `example.input`/`example.output`, with the raw row available as `example.record`. For SFT, the SDK builds the training conversation from the environment's `start_episode(example, prompt_text)` plus `sft_completion(example)`. The default `sft_completion` converts `example.output` into completion messages. See [Datasets](/guides/datasets#message-shaped-sft-targets) for the scalar and message-shaped output forms. Override `sft_completion` only when the gold completion must be synthesized from other fields. Single-turn environments usually implement `build_prompt_messages`. The SDK's default `start_episode` calls it and prepends the run's prompt text as a `system` message when your messages lack one, so local eval, SFT, and GRPO see the same policy prompt. Multi-turn environments own `start_episode`; put any task-specific initial system message there. When a run uses `thinking = true`, `score_response` receives answer text by default. `response_text` stays string-compatible and also exposes `response_text.completion`, `response_text.thinking`, and `response_text.raw` for rewards that inspect the reasoning trace. ## Pass parameters If your `load_environment(**kwargs)` accepts arguments, set them under `[environment.params]`: ```toml theme={null} [environment] id = "your-org/your-project/your-env" [environment.params] difficulty = "hard" num_examples = 500 ``` ## Next steps Handle conversations, tools, and agents. Row shapes and SFT target formats. # Self-hosting Source: https://docs.freesolo.co/guides/self-hosting Run your own Flash control plane against your own GPU accounts. Flash can run as your own control plane against your own GPU accounts, with no Freesolo backend involved. You supply the GPU credentials, you hold the auth key, and you choose which providers to use. This is an operator deployment, not a one-command install: it points the whole training path - SFT, GRPO, and OPD - at hardware you pay for directly. Self-hosting is for teams who want to run on their own GPU accounts. The managed platform at [platform.freesolo.co](https://platform.freesolo.co) needs none of this: [`flash login`](/reference/cli#auth-identity) and [`flash train`](/guides/training) are the whole setup. ## What you need Three settings and at least one GPU account. `flash-server` refuses to start without all four. | Variable | Purpose | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | One GPU provider key | `RUNPOD_API_KEY`, `LAMBDA_API_KEY`, **or** `VAST_API_KEY`. One is enough. | | `HF_TOKEN` | A Hugging Face token with **write** access. Flash streams code, checkpoints, and adapters through Hugging Face dataset repos, so every run needs it on every provider. | | `FLASH_HF_NAMESPACE` | The Hugging Face user or org those repos are created under. Required with `FLASH_STANDALONE=1`: it defaults to Freesolo's namespace, which your token cannot write to. | | `FREESOLO_INTERNAL_KEY` | The key your clients present to your plane. Generate it yourself. | Those four bring up a plane that runs SFT and GRPO. **OPD needs two more**, and without them an OPD submission fails before GPU allocation: `PARASAIL_API_KEY` for the managed teacher, and a worker-reachable HTTPS `FLASH_PUBLIC_URL`. See [Optional pieces](#optional-pieces). `FLASH_HF_NAMESPACE` is **one** id segment - `your-username`, not `your-username/some-repo`. Flash appends the repo name itself, so the `owner/repo` spelling would build a three-segment id that Hugging Face rejects. Startup validates the id it will actually create and refuses the `owner/repo` form by name, rather than letting it pass and fail at your first submit. Everything else is optional. ## Quickstart On the plane host, write the operator config to a file so both the server and your shell read the same values: ```bash theme={null} pip install 'freesolo-flash[server]' # the base install is client-only cat > flash-plane.env <<'EOF' FLASH_STANDALONE=1 HF_TOKEN=hf_... FLASH_HF_NAMESPACE=your-hf-username # where run artifacts are created RUNPOD_API_KEY=... # or LAMBDA_API_KEY, or VAST_API_KEY EOF echo "FREESOLO_INTERNAL_KEY=$(openssl rand -hex 32)" >> flash-plane.env chmod 600 flash-plane.env # it holds your plane's root credential set -a && . ./flash-plane.env && set +a flash-server --host 127.0.0.1 --port 8080 ``` The `server` extra pulls in `runpod-flash`, which installs its own `flash` command. Whichever package was installed last owns the name, so on a plane host `flash` can be RunPod's CLI - and `flash runs cancel ` would then exit 0 without cancelling anything. Flash also installs **`flash-cli`**, a name nothing else claims. Use it on any host with the `server` or `dev` extra, or run `python -m flash.cli`. This page writes `flash` for client commands; substitute `flash-cli` whenever you run one on the plane host itself. `flash-server` runs in the foreground, so leave it running and open a second terminal for the client. Read the key back from the same file rather than retyping it: ```bash theme={null} FREESOLO_INTERNAL_KEY=$(grep '^FREESOLO_INTERNAL_KEY=' flash-plane.env | cut -d= -f2-) flash-cli login --api-url http://127.0.0.1:8080 --api-key "$FREESOLO_INTERNAL_KEY" flash-cli train run.toml ``` `FREESOLO_INTERNAL_KEY` is the plane's root credential, and the quickstart above binds to `127.0.0.1` so it never leaves the host. Before you move the client to another machine, put the connection behind **TLS or an SSH/VPN tunnel** - `--host 0.0.0.0` with a plain `http://` URL sends that key in cleartext to anyone who can observe the network, and it grants full control of your runs, logs, and GPU spending. `flash login` warns when you give it a non-loopback `http://` URL - for `--freesolo-url` as well as `--api-url`, since the key is sent to both (Flash 1.1.40). `flash-server` never terminates TLS itself. For anything but loopback, put a TLS-terminating reverse proxy (nginx, Caddy, a cloud load balancer) in front of it and point `--api-url` at the proxy's `https://` address rather than the plane's own port. Because `--api-url` points at your own plane, `flash login` stores the key and checks it against that plane. It does **not** send it to `api.freesolo.co`: your plane authenticates `FREESOLO_INTERNAL_KEY` itself, and that key controls the plane, so it must not travel to a service you do not run. If you operate your own Freesolo-compatible auth backend, pass `--freesolo-url` and verification happens against it. `flash-server` reads the **process** environment, not `.env`. If you keep credentials in a file, load it first: ```bash theme={null} set -a && . ./.env && set +a && flash-server ``` Kubernetes Secrets, systemd `EnvironmentFile`, and any orchestrator's secret store work as-is. ## Projects on a standalone plane Every run config still needs a top-level `project` UUID - it groups runs and is required in standalone mode too. Any well-formed UUID works, so pick one and reuse it: ```toml theme={null} project = "11111111-1111-4111-8111-111111111111" model = "Qwen/Qwen3.5-4B" algorithm = "sft" [environment] id = "github:your-org/your-repo@main:path/to/env/environment.py" [train] epochs = 1 max_examples = 1000 ``` `flash projects create ` mints a UUID locally for you; pass it to `flash env setup --project `. `flash projects list` has no org directory to enumerate and says so, so keep track of the ids you use. On the plane host, run these as `flash-cli` (see the warning above). ## Environments Point `[environment] id` at a GitHub source the standalone plane can read. Every supported form resolves to an `environment.py` entrypoint. | Form | Resolves to | | ------------------------------------------------------------------- | --------------------------------------------------- | | `github:owner/repo` | `environment.py` on the repository's `main` branch. | | `github:owner/repo@ref` | `environment.py` at the selected ref. | | `github:owner/repo@ref:path/to/env` | `path/to/env/environment.py` at the selected ref. | | `github:owner/repo@ref:path/to/env/environment.py` | That exact entrypoint at the selected ref. | | `https://github.com/owner/repo` | `environment.py` on the repository's `main` branch. | | `https://github.com/owner/repo/tree/ref/path/to/env` | `path/to/env/environment.py` at the selected ref. | | `https://github.com/owner/repo/blob/ref/path/to/env/environment.py` | That exact file at the selected ref. | `ref` may be a branch, tag, or commit SHA; pin a SHA for reproducible runs. Public repos work without credentials, subject to GitHub's unauthenticated rate limit; set `GITHUB_TOKEN` for private repos. The standalone and managed forms do not overlap. A standalone plane rejects `namespace/project/name` and every spelling of Freesolo's managed Hub because that private repository is not readable from the operator's plane. The managed service does the inverse: it accepts only a published `namespace/project/name` slug and rejects direct GitHub references. The check reads `FLASH_STANDALONE` on the **server**, so the plane you submit to decides, not the CLI you submit from. `flash env push` publishes to the managed hub and is not part of a self-hosted deployment. A **local directory is not a supported environment source**: the GPU worker fetches the environment itself, so it needs a source it can reach, and an `[environment] path` is rejected at submit. ## Choosing providers Flash allocates across RunPod, Lambda, and Vast. **Configure the ones you have; the rest are never considered.** A class is eligible only on a configured provider that can actually provision it. Run `flash gpus` for the active class names, VRAM, and estimated hourly rates. | Provider | Variable | Notes | | -------- | ---------------- | --------------------------------------------------------------- | | RunPod | `RUNPOD_API_KEY` | One key, or several comma-separated for multi-account failover. | | Lambda | `LAMBDA_API_KEY` | | | Vast | `VAST_API_KEY` | | Startup fails only when **all three** are missing. By default Flash picks the cheapest validated fitting class across the configured providers. In a run config, scalar `[gpu] type` or `[gpu] provider` is a hard pin. A `type` list names acceptable classes that are cost-ranked together. Ordered `[gpu] providers` is a soft preference: named providers rank first, but other configured providers stay eligible as fallbacks. Do not combine `provider` and `providers`. A single RunPod key works, and warns at startup: a one-account pool cannot ride out that account's quota or credit exhaustion by moving to another. That is an availability property, not a correctness one. Flash does not cap RunPod endpoint concurrency, on a self-hosted plane or a managed one. If you expect many concurrent runs, cap them upstream of Flash or raise the worker quota on your RunPod account - a large enough burst hits RunPod's account limit and the excess deploys fail there. ## What `FLASH_STANDALONE=1` does The managed deployment keeps organizations, projects, and billing in a Freesolo backend and validates against it on every run. A self-hoster has no such backend: without this flag the plane tries to reach `api.freesolo.co`, the validation call fails, and **every run is rejected with a 503**. With it set: * **`FREESOLO_INTERNAL_KEY` is the only credential the plane accepts.** External bearer tokens are rejected rather than accepted unverified, which would turn a self-hosted plane into an open one. * **Project ids are taken as given** - still required and shape-validated, but not checked against an org directory that does not exist here. * **Backend reporting is off** - billing, cost reconciliation, checkpoint registration, and the hosted artifact sweep. Otherwise these would send your operator key to Freesolo. * **Hosted-only CLI commands say so.** `flash projects create` mints a UUID locally; `flash projects list` and `flash traces export` have no local store to read and refuse with the reason. Self-hosting relaxes the billing boundaries, not the catalog: trainable models are the same [curated set](/reference/models) on both deployments, and an uncatalogued id is rejected at config parse time before a GPU is rented. ## The state directory Everything the plane persists locally - the SQLite database of keys and run ownership, run records, results, and the CLI's saved login - lives under one root, `~/.flash` by default. Set `FLASH_DATA_DIR` to move it somewhere a rootless container, a mounted PVC, or a `ProtectHome` systemd unit can write: ```bash theme={null} export FLASH_DATA_DIR=/var/lib/flash ``` It must be **exported**, not just assigned: `flash-server` reads it from its own environment, so a bare shell assignment leaves state under `~/.flash` while you believe it moved. Everything moves together, so back up or mount that one directory. In Docker, setting `FLASH_DATA_DIR` also means mounting your own volume at the new path - the image's `VOLUME` names the default location, and state written anywhere else lands on the container's writable layer and is lost when the container is replaced. Run exactly **one** instance per state directory. State is local files plus SQLite; there is no horizontal scaling. On networked storage, raise `FLASH_SQLITE_BUSY_TIMEOUT_SECONDS` (default 30). ## Logs `flash-server` logs at INFO. Provider resolution, capability revocation, reaper startup, and degraded-configuration warnings are reported there and nowhere else, so that is the first place to look when the plane misbehaves. `FLASH_LOG_LEVEL` turns it up (`DEBUG`) or down (`WARNING`), and `FLASH_LOG_FORMAT=json` emits one JSON object per line for a structured sink. ## Serving `flash serve deploy` provisions serving in **your own Modal or RunPod account**, running the published serving image against one base model and one run's adapter. That is the supported serving path when you are not on managed serving, and it works the same whether your training plane is managed or standalone. See [Serve in your own account](/guides/deploy-and-chat#serve-in-your-own-account) for the full walkthrough and [the CLI reference](/reference/cli#serving-in-your-own-account) for every flag. ```bash theme={null} pip install 'freesolo-flash[server]' # serve needs the server extra export HF_TOKEN=hf_... export FLASH_SERVING_KEY=$(python -c 'import secrets; print(secrets.token_urlsafe(32))') export MODAL_TOKEN_ID=... MODAL_TOKEN_SECRET=... flash serve deploy \ --provider modal \ --model Qwen/Qwen3.5-4B \ --run \ --deployment-id my-4b-serving \ --image ghcr.io/freesolo-co/freesolo-flash-serve@sha256: \ --artifact-repo \ --artifact-subfolder \ --lora-rank 32 \ --modal-workspace \ --modal-environment main \ --modal-region us-east ``` The command prints an endpoint you call directly, authenticated with the `FLASH_SERVING_KEY` you generated. Provider credentials are read for one call and never stored. `flash serve status` and `flash serve undeploy` inspect and remove that exact deployment. Do **not** point `FREESOLO_SERVING_URL` at an endpoint from `flash serve deploy`. `flash models deploy`, `undeploy`, and `chat` authenticate with `X-Freesolo-Internal-Key` and carry `FREESOLO_INTERNAL_KEY` - the key that controls your whole plane. A customer-owned deployment does not read that header, so the request fails `401` after sending a plane-wide credential to a provider endpoint. `flash models deploy`, `undeploy`, and `chat` drive a multi-LoRA backend with a dynamic adapter-registration surface. On a standalone plane they **error out** until you point `FREESOLO_SERVING_URL` at such a backend that you operate; pointing it at a Freesolo-hosted URL is refused, for the credential reason above. A `flash serve deploy` endpoint is not one of these: it receives its adapters in an immutable manifest at boot and serves only `/healthz`, `/v1/models`, and `/v1/chat/completions`. Training, checkpoint streaming, and adapter export never depend on any of this: your adapters land in your own Hugging Face repos, and `flash models export` copies one to a repo you name, ready for vLLM or any LoRA-capable server. ## Optional pieces | Variable | Effect if unset | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `GITHUB_TOKEN` | Environments in **private** GitHub repos cannot be fetched. Public repos still work. Warns at startup. | | `PARASAIL_API_KEY` | OPD submissions fail before GPU allocation. Set it on the control plane; workers never receive it. SFT and GRPO do not use it. | | `FLASH_PUBLIC_URL` | OPD submissions fail before GPU allocation. See [the plane's public origin](#the-planes-public-origin) below. | | `FREESOLO_SERVING_URL` | `flash models deploy`, `undeploy`, and `chat` refuse to run. Training and `flash serve` are unaffected. See [Serving](#serving). | ### The plane's public origin `FLASH_PUBLIC_URL` is **this plane's own public HTTPS origin** - the address a rented GPU worker dials back to reach the teacher broker. It is not the same setting as the client's `FLASH_API_URL` / `--api-url`, which may point at a private address (`http://your-plane:8080`, a tunnel, a VPN) that no rented worker can resolve. An origin workers cannot reach only surfaces **after** a GPU is allocated, so the run has already started costing money by the time it fails. Only OPD uses it; SFT and GRPO never do. Named `FLASH_CONTROL_PANEL_URL` before Flash 1.1.38. The old name is not read. ## The security model **A standalone plane is single-tenant.** It cannot distinguish organizations, so anyone holding `FREESOLO_INTERNAL_KEY` can submit runs, read any run's status and logs, and spend your GPU budget. Treat that key like a root password: generate it with `openssl rand -hex 32`, never commit it, and rotate it if it leaks. Rotating is safe. A standalone plane records run ownership against a fixed single-tenant owner rather than the key's value, so runs started under the old key stay listed, inspectable, and cancellable under the new one, and the old key stops working immediately. Do not point a standalone plane at the state directory of a **multi-tenant** deployment. It has no way to tell whose runs those were and will treat all of them as the operator's. Do not expose a standalone plane to untrusted callers. Put it on a private network, behind a VPN, or behind an authenticating reverse proxy. If you need real multi-tenancy - separate organizations, per-user keys, project ownership enforcement - run without `FLASH_STANDALONE` and set `FREESOLO_BASE_URL` to an identity backend serving the auth-verify contract. ## Verifying your setup `flash-server` preflights at startup: it fails on anything required to run a job, and only warns when a setting degrades an optional capability. It **fails** on no GPU provider configured (all three keys missing), a missing `HF_TOKEN`, a missing `FREESOLO_INTERNAL_KEY`, or - in standalone mode - a missing or unusable `FLASH_HF_NAMESPACE`. It **warns** on a single-account RunPod pool and a missing `GITHUB_TOKEN`, then logs the providers it resolved: ```text theme={null} GPU provider(s) configured: vast ``` That line is the quickest confirmation your credentials were picked up. Then submit a small run and watch it allocate. ## Troubleshooting `FLASH_STANDALONE` is not set, so the plane is trying to reach a Freesolo backend. Set it to `1`. In standalone mode `FREESOLO_INTERNAL_KEY` is the only accepted credential. Confirm the client is sending that exact value (`flash login --api-key ...`, or `flash-cli login` on the plane host) and that the plane was started with it set. It must be a single Hugging Face id segment. `owner/repo` is rejected because Flash appends the repo name itself. Use just the user or org name. The value must be non-empty and non-whitespace. For RunPod specifically, a value of `","` parses to zero usable accounts. The allocator only proposes classes on configured providers. Check the `GPU provider(s) configured:` line at startup. ## Next steps Every field in a training config. The curated catalog a plane can train. # Structured outputs Source: https://docs.freesolo.co/guides/structured-outputs Constrain training rollouts and served responses to valid JSON, a regex, or a fixed choice set with guided decoding. **Guided decoding** forces the model to emit only text that matches a constraint — a JSON schema, a regular expression, or one of a fixed set of choices. Once the grammar is active, the sampler cannot produce off-format answer tokens. You use it in two places: in **training**, so your reward measures the answer's *content* instead of malformed wrappers, and at **serving**, where the training constraint becomes the adapter default and each request can override it. ## In training `structured_outputs` in the `[train]` table constrains every GRPO and OPD rollout. It applies only to the rollout algorithms — SFT never samples, so it rejects the key at submit. When rollouts can't drift off-format, your `score_response` parses the field it cares about directly and rewards the content. ### Write a constraint Set exactly **one** constraint under `[train]`. A JSON schema is the common case; write it as a JSON string or an inline TOML table. ```toml JSON schema (string) theme={null} [train] structured_outputs = '{"type": "object", "properties": {"answer": {"type": "string"}}, "required": ["answer"]}' ``` ```toml JSON schema (inline table) theme={null} [train] structured_outputs = { type = "object", properties = { answer = { type = "string" } }, required = ["answer"] } ``` ```toml Choice theme={null} [train] structured_outputs = { choice = ["yes", "no", "maybe"] } ``` ```toml Regex theme={null} [train] structured_outputs = { regex = "\\d{4}-\\d{2}-\\d{2}" } ``` ```toml Any JSON theme={null} [train] structured_outputs = { json_object = true } ``` | Constraint | Write it as | Forces the output to be | | ----------- | --------------------------------------------------------------------------- | ------------------------------------ | | JSON schema | a JSON string or inline table; key `json` (aliases `json_schema`, `schema`) | JSON matching your schema | | Choice | `{ choice = ["a", "b", ...] }` (alias `choices`), non-empty strings | exactly one of the listed strings | | Regex | `{ regex = "..." }` | text matching the pattern | | Any JSON | `{ json_object = true }`, or the string `"json_object"` | any valid JSON, with no fixed schema | A bare schema table with **no** constraint key is read as a JSON schema — its `type`/`properties` keys are schema vocabulary, not Flash's. Setting more than one constraint, or the unsupported `grammar` / `structural_tag` forms, is rejected at submit. To turn guided decoding off, omit the key or set `false`, `""`, or `"none"`. ### Options These optional keys tune the constraint. Set them **alongside** a constraint (an option on its own is rejected): | Option | Type | Effect | | ------------------------------- | ------ | ------------------------------------------------------------------------------------------------ | | `disable_any_whitespace` | bool | Forbid insignificant whitespace, so JSON comes out compact (and decodes slightly faster). | | `disable_additional_properties` | bool | Reject object properties your schema doesn't declare, even when it omits `additionalProperties`. | | `whitespace_pattern` | string | Custom pattern for the whitespace the sampler may emit between JSON tokens. | ```toml theme={null} [train] structured_outputs = { json = { type = "object", properties = { answer = { type = "string" } }, required = ["answer"] }, disable_any_whitespace = true } ``` ### Thinking mode Structured outputs work with `thinking = true`. For a single-turn rollout and the first assistant turn of a multi-turn rollout, reasoning remains free-form and the grammar begins after the model closes ``. The final answer must satisfy the configured constraint. A later multi-turn prompt may already contain an earlier `` boundary, so its grammar can apply from the first new token. Test later-turn behavior when a multi-turn environment combines thinking and structured outputs. Keep the reasoning and final answer within `max_completion_tokens`. An unclosed or truncated thinking block never reaches the constrained answer and is invalid. ### Score a completed constrained answer For a non-truncated rollout that reaches the constrained final answer, the wrapper is guaranteed. Still return zero if reasoning or generation ends before that answer is complete: ```python theme={null} import json def score_response(self, example, completion: str) -> float: try: answer = json.loads(completion)["answer"] except (json.JSONDecodeError, KeyError, TypeError): return 0.0 return 1.0 if answer.strip() == example.output.strip() else 0.0 ``` ## At serving A deployed adapter or a base model accepts the OpenAI-standard `response_format`. A training `structured_outputs` constraint becomes the deployed adapter's default. A request-level `response_format` overrides that default without a redeploy: ```python theme={null} resp = client.chat.completions.create( model="", messages=[{"role": "user", "content": "Give me a person as JSON."}], response_format={ "type": "json_schema", "json_schema": { "schema": { "type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, "required": ["name", "age"], } }, }, ) ``` `{"type": "json_object"}` forces any valid JSON with no fixed schema, and `{"type": "text"}` leaves output unconstrained. With thinking enabled, reasoning remains free-form and the requested grammar begins after ``. Give each request enough output tokens and check `finish_reason` before parsing the answer. Every real adapter deployment runs a bounded smoke when a default constraint is present. Deployment fails before alias activation if output is invalid or leaves thinking unclosed or truncated. See [Deploy & chat](/guides/deploy-and-chat#structured-outputs). ## Next steps The `structured_outputs` field in the training config. Constrain a served model's responses per request. # Tracing Source: https://docs.freesolo.co/guides/tracing Record your app's LLM calls to a Freesolo project, then export them as a training dataset. Tracing records the chat completions your app already makes and stores them as traces on a Freesolo **project**. Point your existing OpenAI-compatible client at Freesolo's recording endpoint, let it record while your app runs, then pull the traces down with [`flash traces export`](/reference/cli#traces) as [task records](/guides/datasets#task-records) - real production traffic becomes the `dataset/train.jsonl` you train on. Tracing needs an existing **project id**: traces are stored per-project, and a recording request without one is rejected rather than stored nowhere. Create a project in the [dashboard](/platform) or with [`flash projects create`](/reference/cli#projects). Tracing records **chat completions only** (`POST /chat/completions`). The responses, embeddings, images, and audio APIs are not recorded, and neither are non-OpenAI-compatible native clients such as the Anthropic SDK `messages` API. ## How recording works Freesolo exposes one managed OpenAI-compatible recording endpoint at `https://api.freesolo.co/v1`. It is a **pass-through proxy**: it forwards your request to the upstream provider you pick, using a provider key you supply, returns the response unchanged, and stores a trace on the way through. Streaming works too: frames relay unbuffered and the trace is written after the stream completes. Because it is a pass-through, **Freesolo stores no provider credentials and does not bill the inference**. You send your own provider key per request and are billed by that provider directly, separate from [Flash billing](/reference/cost-model). The key transits the proxy as the upstream `Authorization` header without ever being persisted to a trace or a log. Supported upstream providers are `openai`, `anthropic`, `openrouter`, and `google` (Anthropic and Google through their OpenAI-compatibility layers). ### Request headers | Header | Required | Purpose | | ------------------------------------------ | ------------------- | ----------------------------------------------------------------------------- | | `Authorization: Bearer ` | yes | Your Freesolo API key, authenticating the caller's org | | `X-Freesolo-Provider` | yes | Upstream to forward to: `openai`, `anthropic`, `openrouter`, or `google` | | `X-Freesolo-Provider-Key` | yes | Your own key for that provider, forwarded upstream as `Authorization: Bearer` | | `X-Freesolo-Project-Id` | yes, when recording | Project the trace is stored under | | `X-Freesolo-Record` | no | Set to `false` to proxy without recording (defaults to recording) | `X-Freesolo-Project-Id` takes the id (not the name) of a project that already exists in your org; recording never creates one. Omitting it on a recording request returns `400`, and an id outside your org returns `404`. ### Secrets are redacted Before anything is forwarded upstream or written to a trace, the proxy replaces two kinds of values with `[redacted]`: * **Secret-named fields** anywhere in the request or response body: `authorization`, `proxy-authorization`, and keys ending in `api_key`, `secret`, `token`, `password`, `passwd`, `credential`, `credentials`, or `private_key`, compared with case, underscores, and dashes ignored. The `token` suffix is matched in the singular, so `max_tokens` and `completion_tokens` pass through, and JSON Schema definitions (a tool property literally named `token`) are preserved. * **Your credentials by value**: the Freesolo API key and the provider key, wherever they appear, including inside message content. Everything else (`model`, `messages`, tools, `usage`) passes through untouched. ## Record from your app Apply exactly one of these; both record identically through the same endpoint. ### Method A: the drop-in SDK For Python or TypeScript apps, the `freesolo` SDK ships a drop-in that subclasses the official OpenAI client and points itself at the recording endpoint. Do not set the base URL yourself. ```python Python theme={null} # pip install 'freesolo>=0.2.60' import os from freesolo import OpenAI # AsyncOpenAI for async apps client = OpenAI( project_id="", default_headers={ "X-Freesolo-Provider": "openai", "X-Freesolo-Provider-Key": os.environ["OPENAI_API_KEY"], }, ) # unchanged from here on response = client.chat.completions.create( model="gpt-4.1-mini", messages=[{"role": "user", "content": "Refund my last order."}], ) ``` ```typescript TypeScript theme={null} // bun add '@freesolo/sdk@^0.2.60' import OpenAI from "@freesolo/sdk/openai"; const client = new OpenAI({ freesolo: { projectId: "" }, defaultHeaders: { "X-Freesolo-Provider": "openai", "X-Freesolo-Provider-Key": process.env.OPENAI_API_KEY!, }, }); const response = await client.chat.completions.create({ model: "gpt-4.1-mini", messages: [{ role: "user", content: "Refund my last order." }], }); ``` The drop-in reads your Freesolo key from `FREESOLO_API_KEY` and accepts the project id as `project_id` / `freesolo.projectId` (or `FREESOLO_PROJECT_ID`). Carry over the original client's behavioral options (`timeout`, retries, default headers) but **not** `base_url` or `api_key`: the drop-in owns routing and authentication. The provider headers are not set by the SDK itself; pass them as default headers as shown, or the proxy rejects the call with a `400` naming the missing header. ### Method B: the base-URL proxy Any OpenAI-compatible client works (the stock OpenAI SDK, OpenRouter, LiteLLM, the Vercel AI SDK) with no new dependency. Change only the base URL and headers: ```python theme={null} import os from openai import OpenAI client = OpenAI( base_url="https://api.freesolo.co/v1", api_key=os.environ["FREESOLO_API_KEY"], default_headers={ "X-Freesolo-Project-Id": "", "X-Freesolo-Provider": "openai", "X-Freesolo-Provider-Key": os.environ["OPENAI_API_KEY"], }, ) ``` Keep every request argument, including `model` and `messages`, unchanged. Route through the recording endpoint only when `FREESOLO_API_KEY` is present, so your app still works when Freesolo is not configured. If a shared client also serves unrelated calls, build a separate client at the call site you want recorded instead of rerouting the shared one. ## Export traces as a dataset ```bash theme={null} flash traces export --project ``` The export reads the newest 1000 traces in the project and writes them as `{"input", "output"}` records to `dataset/train.jsonl`, exactly the shape [`flash env setup`](/guides/environments/overview#scaffold-one) scaffolds, so it drops into an environment unchanged: ```jsonl dataset/train.jsonl theme={null} {"input":"Refund my last order.","output":"Your last order has been refunded."} ``` `--format prompts` exports `{"input"}`-only rows for [GRPO and OPD](/guides/training#choose-a-training-algorithm), which train from prompts alone, and `--format raw` writes the stored trace rows unconverted. See the [CLI reference](/reference/cli#traces) for the full flag list. `flash traces export` ships in Flash 1.0.19; `--format` needs 1.0.22. ### How a trace becomes a record | Record key | Comes from | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `input` | The request messages, always as a string. A lone user turn exports its bare text; anything longer flattens to role-prefixed lines, so a system prompt or earlier turn is kept rather than dropped. | | `output` | The assistant reply. A tool call is preserved whole as `{"messages": [...]}`; a multimodal reply flattens to its text blocks. | Traces with no extractable request/response pair are skipped, and the CLI reports the count. Because the export is an ordinary dataset, everything in [Datasets](/guides/datasets) applies: a scalar `output` becomes one assistant message, and `{"messages": [...]}` a full gold trajectory for SFT. Rows whose `output` is a tool call score `0.0` under the scaffolded `score_response`, a string containment check that a correct tool call never satisfies. Train those rows with **SFT**, which needs no reward function, or replace the GRPO reward with one that compares the parsed tool call's name and arguments. Text rows are ready for GRPO as scaffolded. ### Seed a new environment from traces You do not have to export by hand and wire the file up yourself. [`flash env setup`](/reference/cli#environments) can seed a new environment straight from a project's traces: ```bash theme={null} flash env setup --project ``` When the selected project has traces and no `dataset/train.jsonl` exists yet, an interactive run offers to seed the dataset from them and sizes `max_examples` to the number of rows it exported. Traces are a head start, never a requirement: decline, or hit an export failure, and setup falls back to the starter dataset. ## View traces in the dashboard Each project has a **Traces** tab in the [dashboard](/platform), with a per-trace detail view and aggregates over the project: score, pass rate, p50 latency, average output tokens and LLM calls per trace, and error rate. The tab can also export: select the traces you want and download them as JSONL, in the same `records`, `prompts`, and `raw` shapes as the CLI's `--format`. The two files are interchangeable; the difference is that the dashboard exports exactly what you select, while `flash traces export` takes the newest 1000 in the project. ## Next steps Everything the exported records can express. Train SFT, GRPO, or OPD on the exported dataset. # Training Source: https://docs.freesolo.co/guides/training Write a config, submit a managed run, and follow it to completion. A training run is one TOML config, submitted with `flash train`. Flash runs the job on managed infrastructure, supervises it, and streams checkpoints and logs back to you. The config and run lifecycle follow; the [configuration reference](/reference/configuration) lists every field. ## Pick a base model Flash trains a [LoRA adapter](/how-flash-works) on top of a supported base model. List base model ids and their parameter sizes ([Supported models](/reference/models) covers algorithms, reasoning, and pricing): ```bash theme={null} flash models list ``` Set your choice at the top of the config: ```toml theme={null} model = "Qwen/Qwen3.5-4B" seed = 42 ``` Flash resolves the supported base-model revision internally so trained adapters remain compatible with managed serving. `seed` controls deterministic training order and defaults to `42`. ## Choose a training algorithm ```toml SFT theme={null} algorithm = "sft" ``` ```toml GRPO (RL) theme={null} algorithm = "grpo" ``` ```toml OPD (distillation) theme={null} algorithm = "opd" ``` * **`sft`**: supervised fine-tuning, when you already have the answers. The model imitates the prompt/answer pairs in your environment's [dataset](/guides/datasets). * **`grpo`**: reinforcement learning, when there's no fixed answer to copy. Your environment's reward scores each completion. * **`opd`**: on-policy distillation, when a stronger model already does the task. A managed teacher, **GLM 5.2 by default** or another selected with `teacher_model`, grades your model's own completions token by token. Training pulls your model toward that teacher without using answers or a reward as the training signal. **Warm-start from an SFT adapter** (`init_from_adapter`) for best results; a cold OPD run tends to underperform SFT. Text-only OPD supports single- and multi-turn environments, but not tool-calling ones. Image-bearing OPD is single-turn only and needs a vision-capable teacher. Teacher access is managed and teacher tokens are not billed to you. All three are driven by the same [environment](/guides/environments/overview), and `algorithm` defaults to `sft` when omitted. See [how Flash works](/how-flash-works) for the difference, or [Examples](/examples) for worked configs and results on real tasks. ## Check the teacher before OPD OPD pulls your model toward the teacher, so the teacher's competence on your task roughly sets the ceiling. Evaluate the teacher before the run; Flash does not automate this comparison. The managed aliases are training-only: Flash resolves `teacher_model` inside the run and exposes no endpoint you can call yourself. Evaluate the **same public model** through its own provider or your evaluation harness, then use the matching alias in your config. The coding agent following the scaffolded [`TRAINING.md`](/directory-structure#trainingmd) can do this for you. 1. Keep a held-out split outside training, then run both the selected teacher and your current model on it. 2. Compare their scores, then read several trajectories from both. A score alone can hide a teacher that reaches the right answer for the wrong reason. 3. Confirm that the teacher clearly wins and that your environment rewards the behavior you want to transfer. | What you find | Next step | | ----------------------------------------------------------------------------- | -------------------------------------------------------- | | The teacher clearly outperforms your model, and the scores match what you see | Use OPD | | The teacher is no better than your model | Use GRPO or train on curated answers with SFT | | The teacher looks better, but the environment does not reward the improvement | Fix the environment, then repeat the held-out evaluation | The environment score does not drive OPD's token-level updates. It tells you whether the behavior you care about improved, so a broken evaluation leaves you unable to judge the run. ### Bound reasoning before long rollouts If the teacher over-reasons in the held-out evaluation or OPD rollouts hit the length limit, put a hard, specific budget in the environment's system prompt: ```text theme={null} Reason in at most two or three sentences, then act. Once you have started, do not reconsider. ``` Avoid a vague instruction such as "be brief." It can shorten the typical response while making the longest responses even longer. Re-run the held-out evaluation after changing the prompt. See [Troubleshooting](/reference/troubleshooting) if rollouts still fail to terminate. ## Anatomy of a config ```toml theme={null} project = "" # required, from `flash projects list` model = "Qwen/Qwen3.5-4B" algorithm = "sft" seed = 42 # thinking = true # opt into reasoning mode [environment] id = "your-org/your-project/your-env" [train] epochs = 3 # passes over retained rows; ignored below, max_steps wins max_examples = 1000 # sft keeps the first n rows before seeded shuffle max_steps = 100 # exact update horizon; drop it to derive from epochs save_at_steps = [25, 50, 100] # mandatory saves; requires positive max_steps lora_rank = 32 # learning_rate = 1e-4 # batch_size = 8 # sft only; grpo/opd reject it # prompts_per_step = 64 # grpo/opd only: prompts per optimizer update # group_size = 8 # grpo: completions per prompt (2, 4, or 8) # max_completion_tokens = 512 # grpo/opd: generated tokens per rollout turn # max_context_tokens = 4096 # total context cap, including sft # lora_alpha = 64 # defaults to 2 x lora_rank; set to depart from that # init_from_adapter = "" # any algorithm; omit lora_rank/lora_alpha (inherited) # teacher_model = "glm-5.2" # opd: managed teacher # structured_outputs = '{"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"]}' [gpu] # count = 4 # omit to auto-size; set it to cap the cards [wandb] # project = "my-project" # optional weights & biases logging ``` There are no `[sft]`, `[grpo]`, or `[opd]` tables. Every knob lives under `[train]`, and one the run's algorithm cannot consume - `group_size` on SFT, `teacher_model` on GRPO - is **rejected at submit** rather than silently ignored. See [knobs are scoped by algorithm](/reference/configuration#knobs-are-scoped-by-algorithm). To force every GRPO or OPD rollout into valid JSON, a regex, or a fixed choice set, so your reward scores content instead of formatting, set `structured_outputs`. See the [Structured outputs](/guides/structured-outputs) guide. ## Control the update horizon and checkpoints For SFT, GRPO, and OPD, a positive `train.max_steps` is the exact number of optimizer updates, and `epochs` no longer affects the horizon. If `max_steps` is absent or non-positive, Flash derives the update count from epochs, retained examples, the optimizer batch (`batch_size` for SFT, `prompts_per_step` for GRPO and OPD), and the recipe. A GRPO or OPD config that sets neither is refused rather than derived, since nothing measures the prompt pool - see [GRPO and OPD need a stated horizon](/reference/configuration#grpo-and-opd-need-a-stated-horizon). Use `train.save_at_steps` when specific checkpoint boundaries matter: ```toml theme={null} [train] max_steps = 100 save_at_steps = [25, 50, 100] ``` The list must be strictly increasing and positive, requires positive `max_steps`, and cannot contain a step beyond it. It suppresses `save_every`, and every requested save is mandatory. Without `save_at_steps`, periodic `save_every` uploads block training while attempted but remain best-effort after bounded retries. For SFT, `max_examples = N` retains the first N rows in file order before the seeded shuffle. SFT also honors `max_context_tokens` for the rendered sequence. ## Warm-start safely `init_from_adapter` continues a source adapter in a new run, and it works **between every pair of algorithms**. SFT, GRPO, and OPD each read a source adapter produced by any of the three, including same-algorithm continuation: ```toml theme={null} algorithm = "grpo" # or "sft", or "opd" [train] init_from_adapter = "" # from an sft, grpo, or opd run ``` The source can be a finished run or a saved `RUN_ID/step-N` checkpoint from `flash runs checkpoint`. Common shapes: | Warm start | Why | | --------------- | --------------------------------------------------------- | | `sft` → `grpo` | Teach the format first, then optimize past it with reward | | `sft` → `opd` | Recommended: a cold OPD run tends to underperform SFT | | `sft` → `sft` | Keep training the same adapter on more data | | `grpo` → `grpo` | Extend a reinforcement-learning run | Keep the **same base model** across the lineage. The source adapter's LoRA rank and alpha are authoritative, so omit both `lora_rank` and `lora_alpha` - setting either alongside `init_from_adapter` is rejected, as is a source rank above the model's serving cap. `--dry-run` and a real submit both resolve the source rank for you; local `--cost` may warn and estimate with defaults instead. A warm-started SFT run inherits its source's base-model pin instead of resolving its own, so the continued adapter stays deployable when the base model's upstream tip moves. Use `--dry-run` to resolve and validate the source adapter before submission. ## Infrastructure is managed You choose the model, algorithm, environment, and training settings. Flash picks the cheapest validated GPU shape that fits by default, while `[gpu]` can constrain class, provider, and card count. Run `flash gpus` for active class names, VRAM, and estimated hourly rates. See [GPU configuration](/reference/configuration#gpu) and [Multi-GPU runs](/reference/configuration#multi-gpu-runs) for the exact pinning, preference, fallback, and auto-sizing rules. Storage remains managed. ```bash theme={null} flash train config.toml --gpus 4 ``` ## Validate before you submit `--dry-run` runs the same submit-time checks as a real run, including warm-start resolution, and flags any `[train]` keys your CLI version does not recognize. It does not start paid training or allocate a training GPU: ```bash theme={null} flash train config.toml --dry-run ``` For SFT, dry-run obtains the same static estimate used by `--cost`, without starting paid training or allocating a training GPU. Use `--cost` separately for the pre-flight estimate; see [Cost and billing](/reference/cost-model). ## Submit the run ```bash theme={null} flash train config.toml ``` By default `flash train` follows the logs until the run finishes; `--background` submits and returns immediately. `--set` overrides a config value and `--config` deep-merges another TOML file, both repeatable. See [Training](/reference/cli#training) for every flag. ```bash theme={null} # override values at submit time flash train config.toml --set train.epochs=3 --set train.lora_rank=16 ``` ## Cost and billing Review the estimate before submitting: ```bash theme={null} flash train config.toml --cost ``` GRPO and OPD estimate locally. SFT returns a static packaged-dataset estimate synchronously through the control plane. Successful runs bill at the accepted quote; cancellations are prorated and capped at that quote. See [Cost and billing](/reference/cost-model). ## Monitor a run `Ctrl-C` during `flash train` detaches you. The run keeps going on Freesolo. ```bash theme={null} flash runs list # all your runs: state, cost, model flash runs status --json # one status and cost JSON object flash runs status --follow --json # changed statuses as JSON Lines flash runs log # print the full log snapshot flash runs log --follow # stream logs until completion flash runs cancel # cancel a run ``` ### Reading logs from a retried run A run that was retried has more than one worker attempt, and their logs are different runs of your job. `flash runs log` names the attempt each section came from, and marks heartbeats from a worker that has been superseded or torn down: ```text theme={null} [superseded attempt=0; worker torn down] ``` Untagged heartbeats are the live attempt. This matters when you skim to the last heartbeat to check progress - without the tag, a dead worker's final lines look like current progress. Log artifacts are kept even when the attempt they belong to cannot be identified, so a lookup failure never costs you the output. ## After training When a run reaches `done`, serve it with [`flash models deploy`](/guides/deploy-and-chat) and talk to it with `flash models chat`. Deploy or [export](/guides/deploy-and-chat#export-to-your-own-huggingface-repo) a run you want to keep. Freesolo's managed storage garbage-collects a run's checkpoints and final adapter about **7 days** after its last activity when the run was never deployed. A deployed run, or one that another run warm-started from, is kept. Exporting copies the adapter into a HuggingFace repo you own. ## Next steps Serve a finished run and talk to it. Every field you can set in a training config. # How Flash works Source: https://docs.freesolo.co/how-flash-works Post-training in plain terms: the few concepts that matter, and how Flash runs them for you. ## What post-training does A base model like Qwen3.5 arrives from **pre-training** fluent but generic. **Post-training** shapes it into one that is reliably good at *your* task, using *your* data and *your* definition of a good answer. You describe the task and pick a base model; Flash fine-tunes it on managed infrastructure and serves the result behind an OpenAI-compatible API. ## The training loop Every post-training run has the same three moving parts: a **base model**, an **environment** (your task and how it is graded), and a **training algorithm** that improves the model from a feedback signal. What differs is where that signal comes from. Under SFT it also takes that row's gold answer. Under GRPO and OPD the current model generates its own attempts at the prompt. **SFT** compares against the gold answer in your dataset. **GRPO** scores each attempt with your environment's reward. **OPD** has a managed teacher grade your model's tokens, so your reward is not the training signal. The weights move toward whatever that signal rewards: the gold completions, the higher-scoring attempts, or the teacher's distribution. Over many steps the model gets measurably better at the task. The output is a small **adapter** you can deploy. ## Core concepts ### Base models and LoRA adapters Flash trains a **LoRA adapter**: a small set of extra weights layered on top of the frozen base model. This is *parameter-efficient* fine-tuning, with three practical payoffs: * **Cheap and fast** to train, because you're updating a tiny fraction of the parameters. * **Small to store and move** (megabytes, not gigabytes). * **Efficient to serve**, because many adapters that share a base model can be served by the same managed service. Pick the base model with one line in your config. Browse the catalog with `flash models list`, or see [Supported models](/reference/models). ### Environments: your task, as code An **environment** is the task, expressed as code: a **dataset** of prompts your model practices on, and a **reward** that scores an answer. It is the single source of truth for *what the model practices on* and *how it's graded*. You author one locally, publish it to the managed Environments Hub, and reference it from your config by Freesolo id. See the [Environment model](/environment-model) for the concepts, or [Environments](/guides/environments/overview) and [Datasets](/guides/datasets) to build one. ### Three ways to teach: SFT, GRPO, and OPD There are three ways to turn that environment into a better model, and you choose between them with one line of config. **Supervised fine-tuning.** You show the model good answers and it learns to reproduce them. Best when you already have examples of the behavior you want. **Reinforcement learning.** The model generates attempts, your reward scores them, and the model is pushed toward the higher-scoring ones. Best when good output is easier to *score* than to *write out* by hand. **On-policy distillation.** The model generates attempts and a stronger teacher grades them token by token; the model is pulled toward the teacher. Best when a bigger model already does the task and you want a small one to match it — no answers to write, no reward to design. Pick by what you can supply. See [Training](/guides/training#choose-a-training-algorithm) to configure one. ### Rewards and rollouts (GRPO) In GRPO, a **rollout** is one attempt the model generates for a prompt. For each prompt, GRPO samples a *group* of rollouts (the `group_size`), scores each one with your environment, and reinforces the rollouts that beat the group's average. The **reward** is the score your environment returns. The reward is the teacher. If it reliably separates good answers from bad ones, GRPO can optimize toward it; if the task is so hard that every rollout scores zero, there's no signal to learn from, so start with a model and task where some attempts succeed. ### The teacher (OPD) In OPD, a **teacher** — a managed model, GLM 5.2 by default or another you pick with `teacher_model` — grades your model's own completions token by token, and training pulls your model toward the teacher's distribution. Freesolo manages the teacher and its credentials, so there's nothing to set up and you don't pay for teacher tokens — only the GPU time the run uses. Because the signal only refines tokens your model already produces, OPD works best when you **warm-start from an SFT adapter** that already gets the format right; a cold OPD run tends to underperform plain SFT. ### Serving the result Deploy the adapter to call it. `flash models deploy` registers your adapter with Freesolo's **managed serving**. You talk to it over an OpenAI-compatible API, and serving is billed per token. See [Deploy & chat](/guides/deploy-and-chat). ## Is post-training right for your task? It fits when the task is narrow and you can define success: you want a **small, cheap model** to do one job reliably instead of paying for a frontier model on every call, and prompting gets you *close* but not **consistent**. If you have neither data nor a way to grade answers, start there - the quality of your environment sets the ceiling on what training can achieve. ## Next Train, deploy, and chat with your first model in a few minutes. SFT, GRPO, and OPD, config options, monitoring, and cost. # Flash Source: https://docs.freesolo.co/index Train, deploy, and chat with custom LoRA models on managed infrastructure, from one CLI. Flash is Freesolo's managed post-training service. Write a short config, run one command, and Flash fine-tunes a model on managed infrastructure, then serves the result behind an OpenAI-compatible endpoint. Nothing to host. Every command that talks to the platform authenticates with your Freesolo API key. Install the CLI and go from an empty directory to a deployed model in a few minutes. ## What you can do ### Fine-tune a model on your own task Write a TOML config, run one command, and Flash trains a LoRA adapter (a small set of add-on weights) on top of a [supported base model](/reference/models). Pick the model and task; Flash handles the training infrastructure. See [Training](/guides/training). ```bash theme={null} flash train config.toml ``` ### Pick how the model learns One line of config selects the algorithm. Use **SFT** when you already have example answers, **GRPO** when you can score an output but can't hand-write the perfect one, and **OPD** when a stronger model already does the task and you want a small one to match it. See [Training](/guides/training#choose-a-training-algorithm). ```toml theme={null} algorithm = "sft" # or "grpo", or "opd" ``` ### Serve it behind an OpenAI-compatible API `flash models deploy` registers the adapter with managed serving, then `flash models chat` or any OpenAI client can call it with your Freesolo key. See [Deploy & chat](/guides/deploy-and-chat). ```bash theme={null} flash models deploy ``` ### Or serve it in your own cloud account `flash serve deploy` provisions the same serving stack in **your own Modal or RunPod account**, so the GPU runs and bills there and you call the provider's endpoint directly. See [Serve in your own account](/guides/deploy-and-chat#serve-in-your-own-account). ```bash theme={null} flash serve deploy --provider modal --model Qwen/Qwen3.5-4B --run ... ``` ### See the cost before you spend `--cost` returns a pre-flight estimate without starting paid training or allocating a training GPU. Successful runs bill at the accepted quote, cancellations are prorated and capped at that quote, and serving is billed per token. See [Cost and billing](/reference/cost-model). ```bash theme={null} flash train config.toml --cost ``` ## Next steps Install the CLI, log in, and ship your first run in a few minutes. The loop behind a run: base models, environments, algorithms, serving. Write a config, submit a run, and follow it to completion. Serve an adapter, then chat with it over an OpenAI-compatible API. # Platform dashboard Source: https://docs.freesolo.co/platform What you can see and manage in the Freesolo web app when you sign in. Flash runs from the `flash` CLI. The same account has an org-scoped web dashboard at [platform.freesolo.co](https://platform.freesolo.co). Product updates are published in the [changelog](/changelog). ## Projects The dashboard groups work into org-level **projects**: training runs, published environments, and recorded traces are organized by project, and the navigation is scoped to the project you have open. A **project selector in the top bar** switches between projects from anywhere in the dashboard. A new organization starts with a project called `Example`, holding a starter environment and one example trace, so the shape of the workflow is visible before you train anything. The environment is published to your org's Hub namespace moments after the org is created, so it is briefly pending on a brand new org. It is an ordinary project: nothing selects it implicitly, and it carries no special protection, so you can rename or delete it like any other. Every project-scoped operation names its project explicitly. There is no default, no fallback, and no implicit selection: `flash env push` and `flash env delete` take a required `--project`, a [training config](/reference/configuration#top-level) sets a required top-level `project` field, and [trace recording](/guides/tracing) requires an explicit project id. A missing, malformed, or inaccessible project fails the command rather than picking one for you, so a run can never land in a project you did not name. Create projects in the dashboard or with [`flash projects create`](/reference/cli#projects), and list the ones you can reach with `flash projects list`. ## API keys Commands that contact Freesolo authenticate with a Freesolo API key. Create and revoke keys in **API Keys**. Keys are org-wide; any org member can manage them. A new key is shown once. Use it with `flash login --api-key ` or the `FREESOLO_API_KEY` environment variable. See [Quickstart](/quickstart#step-2-log-in). ## Training runs **Training Runs** lists the runs submitted with [`flash train`](/guides/training). Filter by status; columns cover state, model, environment, and cost. Open a run for the same detail exposed by `flash runs status`: its configuration, checkpoints, deployment details, error state, and current or final cost record. For GRPO and OPD runs, the run page also shows a **sampled completions** panel: per training step it surfaces example prompts and the completions the model generated, each tagged with its GRPO reward or OPD distillation loss, so you can watch what the model produces as it trains. ## Checkpoints A run detail page lists its **deployable checkpoints**: the per-step adapters available to serve, including intermediate checkpoints from a run that stopped mid-training. This is the same set you get from [`flash runs checkpoint `](/reference/cli#run-management); deploy one with `flash models deploy /step-`. See [Deploy a specific checkpoint](/guides/deploy-and-chat#deploy-a-specific-checkpoint). ## Environments **Environments** lists the private Hub environments you publish with [`flash env push`](/guides/environments/package#publish-it). A managed environment id is `namespace/project/name`, where `namespace` is your org slug and `project` is the slug of the project that published it. The page tracks publish time, last use, and run count. From it you can inspect, pull, or delete environment source. ## Traces Each project has a **Traces** tab for the LLM calls your app [records](/guides/tracing) through Freesolo's recording endpoint, with a per-trace detail view and project aggregates: score, pass rate, p50 latency, average output tokens and LLM calls per trace, and error rate. Select traces to export them as a [dataset](/guides/datasets#task-records) you can train on; see [View traces in the dashboard](/guides/tracing#view-traces-in-the-dashboard) for how that compares with `flash traces export`. ## Models **Models** lists the org's deployed LoRA adapters from [managed serving](/guides/deploy-and-chat). Each deployed run appears as one stable run-alias row, not separate rows for immutable revisions. It shows the adapter id, [base model](/reference/models), serving URL, latest reward when the adapter came from a Flash run, per-token billing, and serving status. The alias routes only to a revision that passed deployment verification, while older immutable revisions can remain directly callable. Rows that still have a matching Flash run link back to the run detail page. Open a model for its **deployment detail**, where you can start and tear down serving without switching to the CLI. Deploying from the dashboard runs the same verification as [`flash models deploy`](/guides/deploy-and-chat): the run alias begins routing only once the new revision answers a smoke request, and undeploying stops further requests to that adapter, and with them the per-token charges. A deployed adapter you send no requests to costs nothing. Model access is org-scoped: the serving endpoint authorizes external chat requests against the org that owns the adapter. ## Billing Flash is prepaid. Billing settings show the org balance, activity, payment method state, manual top-ups, and auto-top-up configuration. Any org member can view the summary; owners and Freesolo team members manage payment methods and top-ups. Starter credit comes in two tiers, \$60 in total, each claimed once from billing settings: \$10 with no payment method at all, and a further \$50 once a card is on file. Adding a card does not credit your balance on its own. Manual top-ups range from \$50 to \$10,000. The activity log itemizes: * Training runs, [charged after successful completion](/reference/cost-model#charges-and-cancellations) at the Flash cost quote. Setup time is reported but not billed. A cancelled run is prorated from its accepted quote by the share of the work it completed, capped at that quote. * Serving usage, [charged per token](/reference/cost-model#serving-billing). # Quickstart Source: https://docs.freesolo.co/quickstart Install the CLI, log in, and train, deploy, and chat with your first model. Go from an empty directory to a deployed model. Train a [LoRA adapter](/how-flash-works) on managed infrastructure, serve it, and chat with it from the CLI. ## Prerequisites * **Python 3.11 or 3.12**, with `uv`, `pipx`, or `pip` to install the CLI. * **A Freesolo API key**, created in your dashboard at [freesolo.co](https://freesolo.co). Commands that contact Freesolo, including `--dry-run` and an SFT `--cost`, authenticate with it. A GRPO or OPD `--cost` quotes offline from the catalog. These checks do not start paid training or allocate a training GPU. ## Step 1: Install the CLI ```bash uv theme={null} uv tool install freesolo-flash ``` ```bash pipx theme={null} pipx install freesolo-flash ``` ```bash pip theme={null} pip install freesolo-flash ``` ## Step 2: Log in ```bash theme={null} flash login --api-key ``` Flash verifies the key against Freesolo and stores it locally, so you only do this once. You can also set `FREESOLO_API_KEY` instead of passing `--api-key`. ```bash theme={null} flash whoami # confirm who your stored key resolves to ``` ## Step 3: Create a project Every run and environment belongs to a [project](/platform#projects). Create one and keep its UUID; the next steps need it. ```bash theme={null} flash projects create my-project ``` This prints the project UUID. `flash projects list` shows them all later. ## Step 4: Scaffold an environment Already have a published environment id, yours or one shared with you? Set it as `[environment] id` in your config and skip ahead to step 6. ```bash theme={null} flash env setup --project ``` This writes a ready-to-edit starter into the current directory: ``` environment.py # a Freesolo environment (task + reward) evaluations.py # held-out eval suites for flash env eval dataset/ train.jsonl # a tiny starter dataset (input/output rows) configs/ sft.toml # an SFT training config to start from rl.toml # a GRPO (RL) training config to start from opd.toml # an OPD (distillation) training config to start from TRAINING.md # agent playbook with common run issues and mitigations ``` Rerunning is safe: `flash env setup` leaves any file that already exists untouched. Skip the hand-editing. Point your coding agent (Claude Code, Cursor, etc.) at the [environment guide](/guides/environments/overview) and have it find and port your existing reward and dataset into `environment.py`. A prompt to start from: ```text theme={null} I already have a training loop with a reward function and a dataset, and I want to run it on Freesolo Flash. Find my reward function and dataset in this project, then read https://docs.freesolo.co/guides/environments/overview and create an environment.py whose load_environment() returns an EnvironmentSingleTurn (use EnvironmentMultiTurn for multi-step tasks). Port my reward into score_response and wire my dataset in as the input/output records load_environment() returns. Then fill in the matching training config in configs/ (sft.toml for SFT, rl.toml for GRPO, opd.toml for OPD) with the base model and [train] settings. ``` ## Step 5: Publish your environment An [environment](/guides/environments/overview) is the task and reward your model trains on. [Publish](/guides/environments/package#publish-it) the scaffolded one to the managed Environments Hub to get an id: ```bash theme={null} flash env push --project --name starter . ``` This prints an environment id of the form `your-org//starter`. ## Step 6: Configure and validate your run `flash env setup` already wrote `project` into each generated config. Open `configs/sft.toml` and set the one thing that's yours, the environment id from the previous step: ```toml configs/sft.toml theme={null} model = "Qwen/Qwen3.5-4B" project = "" algorithm = "sft" [environment] id = "your-org/your-project/starter" [train] epochs = 1 max_examples = 2 lora_rank = 32 ``` Validate it first. `--dry-run` applies the real submit-time checks, including unrecognized `[train]` keys, without starting paid training or allocating a training GPU: ```bash theme={null} flash train configs/sft.toml --dry-run ``` For SFT, `--cost` returns the estimate directly without starting paid training or allocating a training GPU. Review the [cost details](/reference/cost-model) before submitting: ```bash theme={null} flash train configs/sft.toml --cost ``` `--cost` prints the pre-flight USD estimate and returns. ## Step 7: Train ```bash theme={null} flash train configs/sft.toml ``` This is the first flow command that starts paid training. Flash checks your org balance before submission and bills successful runs at the accepted quote. Flash then follows the logs live. Press `Ctrl-C` to detach. The run keeps going on the server, and you can [follow it again any time](/guides/training#monitor-a-run): ```bash theme={null} flash runs list # list your runs and their state/cost flash runs status --json # status JSON, including the cost record flash runs log -f # stream logs until the run finishes ``` The run reaches `done` when training finishes. Start small: finish one short run end to end before you scale up. When you do, raise `train.epochs` or `train.max_examples` and change little else. ## Step 8: Deploy Serve the trained adapter on Freesolo's [managed serving service](/guides/deploy-and-chat). Serving is [billed per token](/guides/deploy-and-chat#billing) for requests you send: ```bash theme={null} flash models deploy --wait ``` Keep `--wait` so the chat step starts only after the deployment is ready. You can also deploy and tear down from the [dashboard](/platform#models). ## Step 9: Chat ```bash theme={null} flash models chat -m "Hello! What can you do?" ``` That completes the loop. When you're done, [tear the endpoint down](/guides/deploy-and-chat#manage-deployments): ```bash theme={null} flash models undeploy ``` ## Essential commands The commands you used above, plus the ones you'll reach for next. Run `flash --help` for the full set of flags, or see the [CLI reference](/reference/cli). | Command | What it does | Example | | ----------------------- | ------------------------------------------- | -------------------------------------------------- | | `flash login` | Store your Freesolo API key locally | `flash login --api-key ` | | `flash projects create` | Create a project and print its UUID | `flash projects create my-project` | | `flash projects list` | List your projects and their UUIDs | `flash projects list` | | `flash env setup` | Scaffold a starter environment and configs | `flash env setup --project ` | | `flash env push` | Publish an environment and print its id | `flash env push --project --name starter .` | | `flash env eval` | Score held-out suites against a deployment | `flash env eval ` | | `flash train` | Submit a run and follow its logs | `flash train configs/sft.toml` | | `flash train --dry-run` | Validate without paid training or a GPU | `flash train configs/sft.toml --dry-run` | | `flash train --cost` | Preview cost without paid training or a GPU | `flash train configs/sft.toml --cost` | | `flash runs list` | List your runs with state, cost, and model | `flash runs list` | | `flash runs status` | Show run status and cost | `flash runs status ` | | `flash runs log` | Print or follow a run's logs | `flash runs log -f` | | `flash models deploy` | Serve a trained adapter | `flash models deploy ` | | `flash models chat` | Send a message to a deployment | `flash models chat -m "hi"` | | `flash models undeploy` | Tear a deployment down | `flash models undeploy ` | ## Next steps The loop behind a run, and the concepts each command refers to. SFT, GRPO, and OPD, config options, monitoring, and cost. Replace the starter task with your own data and reward. Serving billing and the OpenAI-compatible API. # CLI reference Source: https://docs.freesolo.co/reference/cli Every flash command and flag. The CLI is `flash`. Run `flash --help` or `flash --help` for inline help. ## Global flags | Flag | Effect | | ----------------- | --------------------------------------------------------------------- | | `-V`, `--version` | Print the Flash version and exit | | `--debug` | Show full tracebacks on error (otherwise errors print one clean line) | | `-v`, `-vv` | Increase log verbosity (`-v` info, `-vv` debug) | Run-management and serving commands are grouped under `flash runs` and `flash models`. ## Auth & identity Log in with your Freesolo API key. Flash verifies the key against Freesolo, saves it locally, and prints the resolved identity on success. Defaults: `--api-key` from `FREESOLO_API_KEY`; `--freesolo-url` from `FREESOLO_BASE_URL` (else `https://api.freesolo.co`); `--api-url` from `FLASH_API_URL`. Show the identity your stored key resolves to. Print the Flash version. ## Projects Every training run and every published environment belongs to one Freesolo [project](/platform#projects), named by its UUID. Configs carry it as the top-level `project` field; `env push` and `env delete` take it as `--project`. Create a project in your organization and print its UUID. Names are unique per org, so reusing one is rejected. On a styled terminal the UUID is printed with its name; otherwise the bare UUID is printed, so it captures cleanly in a script (`PROJECT=$(flash projects create my-project)`). List your organization's projects with their UUIDs. Piped or redirected output is one tab-separated `UUIDNAME` per line. Use this to find the UUID for a config's `project` field or a `--project` flag. ## Discovery List the supported base model ids. See [Supported models](/reference/models) for algorithms, reasoning, and pricing. List validated managed GPU classes with VRAM and estimated \$/hr. The command does not report live capacity or per-provider availability. Selection is automatic by default; `[gpu] type` accepts either one pinned class or a list of acceptable classes. ## Environments Scaffold a [starter project](/directory-structure) into the current directory: `environment.py`, a starter `evaluations.py`, a tiny `dataset/train.jsonl`, `configs/sft.toml` (SFT), `configs/rl.toml` (GRPO), and `configs/opd.toml` (OPD), plus a `TRAINING.md` playbook for coding agents with current CLI usage, reward guidance, and common run mitigations. Existing files are preserved. Setup needs a project, so run `flash login` first. `--project` is required without a TTY or with `-y`; otherwise an interactive run prompts. The UUID is written into every generated config. If configs already exist, setup refuses to continue when one has no valid `project` or names a different one, rather than rewriting them. An interactive run surveys the interaction shape and whether to train with reasoning, then scaffolds to match; the flags below skip the questions. If the project has [recorded traces](/guides/tracing) and no `dataset/train.jsonl` exists, it also offers to seed the dataset from them and sizes `max_examples` to the export. An existing dataset is never overwritten. Generated configs now carry a filled-in [`[wandb]`](/reference/configuration#wandb) block: `project` from the Freesolo project you selected and `run_name` from the folder and algorithm. Edit either value freely, or delete the block if you do not use Weights & Biases. | Flag | Default | Effect | | ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--project PROJECT_UUID` | prompt | Project for the generated configs and environment publication. Required without a TTY or with `-y` | | `--single-turn` | default | Scaffold an `EnvironmentSingleTurn` env (prompt → one response) | | `--multi-turn` | - | Scaffold a bounded-episode `EnvironmentMultiTurn` starter (`start_episode` / `step_episode` / `max_episode_turns` / `score_episode`) with a matching dataset | | `--reasoning` | - | Scaffold configs with reasoning on (`thinking = true`, and a raised GRPO completion budget) | | `--no-reasoning` | default | Scaffold configs without reasoning | | `-y`, `--yes` | - | Accept the defaults without prompting | Validate a local environment offline before publishing. Loads the same entrypoint `flash env push` would package (`PATH` defaults to `.`) and drives its first few dataset episodes with no GPU and no model: it checks that prompts, SFT completions, and any multi-turn replies are well-formed chat messages and that the reward is finite, printing a short prompt/response preview per episode. It replays the dataset's gold `sft_completion` as the policy answer when one is present, otherwise a canned response. Prints `overall: PASS` and exits `0` when every driven episode passes the contract checks, or `overall: FAIL` and exits `1` on a load error, an empty dataset, malformed messages, a hook exception, or a non-finite reward. `--algorithm` must match the algorithm you plan to train and defaults to `grpo` so omitting it cannot disable the reward gate. The GRPO-only gate fails when every accountable replayed gold answer scores zero, no valid per-turn reward vector provides separation, and a deliberately wrong answer scores at least as well. Under `--algorithm sft` or `opd`, an all-zero reward is advisory rather than blocking because those algorithms do not train from `env.reward`. Episodes with no gold answer to replay are exempt. `--split` selects the dataset split to drive, matching `[environment.params]` and defaulting to `train`. Use the split the run actually trains on, for example `--split train_sft`. `--param KEY=VALUE` is repeatable and passes any other `load_environment()` keyword argument; values parse as TOML scalars, so `1`, `true`, and `"x"` keep their types. `--split` wins over `--param split=...`. Score a deployed model against the held-out suites in the published environment its training run used. `TARGET` is a bare `RUN_ID`, a `RUN_ID/step-N` checkpoint, or a full immutable adapter revision. There is no environment `PATH` positional: Flash reads the target run, downloads that published managed environment package, and loads `environment.py` plus `evaluations.py` from that package. A run without a published `namespace/project/name` environment is refused, so this command is not available for standalone runs that use generic GitHub references. Define the suites in an `evaluations.py` beside the published `environment.py`: ```python theme={null} from flash.envs.evaluations import BaseEvalSuite, EvalCase class HeldOut(BaseEvalSuite): name = "held-out" def cases(self): return [EvalCase(id="sum", input="2+2", expected="4")] def load_evaluations(environment=None): return [HeldOut()] ``` `score()` may return an `EvalResult`, a float, or a bool. A module-level `EVALUATIONS` list works instead of the factory, and the factory receives the loaded environment when it accepts one, so suites can reuse environment graders. `--split` and repeatable `--param KEY=VALUE` configure `load_environment()` in the same way as `env test`. Values parse as TOML scalars, and `--split` wins over `--param split=...`. `--suite` runs only the named suite, `--max-cases` caps the cases taken from each selected suite, and `--concurrency` (default `1`, maximum `32`) issues that many model requests in parallel while results stay in case order. Generation defaults to `--temperature 0.0` and `--max-tokens 512`. Results upload to the dashboard by default under the project that owns the target run. `--project PROJECT_ID` selects another accessible project. There is no first, sole, or example-project fallback. Pass `--no-upload` to score without recording results; `--project` and `--no-upload` cannot be combined. A multi-turn environment still runs each suite as a one-response check by default. Set `grades_episodes = True` on a suite to play one generation per turn. A state-aware scorer can define `score(case, response, state)` to receive the finished transcript state; a two-argument scorer receives only the final response text. Episode suites are rejected against single-turn environments. A case that never reached the model is recorded as an error and excluded from `pass_rate` and `mean_score` rather than scored as zero, so a broken deployment does not read as a low-quality model. Errors are printed, uploaded as failures when upload is enabled, counted in the summary, and still fail the command. Publish a local Freesolo environment to Freesolo's [managed Environments Hub](/guides/environments/package#publish-it) (private) and print its id (`your-org/your-project/name`). `--name` is required and is normalized to a lowercase hyphen slug. You can also pass an explicit `namespace/project/name`; Freesolo validates both against your org namespace and the `--project` you passed, and rejects a mismatch. `PATH` defaults to the current directory. Pass `.` or any folder with `environment.py` at its root to upload helper modules, `dataset/`, `README.md`, and common sibling sidecars ([what gets uploaded](/guides/datasets#what-gets-uploaded) lists the extensions). A single `.py` file, or a folder with one top-level `.py` file, also works for small smoke tests; single-file mode packages only that entrypoint, a sibling `README.md`/`TRAINING.md`, and any `dataset/` tree. Secrets (`.env` files, `*.key`, `*.pem`, `credentials*`, SSH keys) and virtualenvs are never uploaded. `--project` is required and must be a project UUID from your organization (`flash projects list`). A malformed UUID is rejected before upload, and one that does not belong to your org fails the push. There is no fallback project. Environment names are unique per project, so two projects in your organization can each publish their own `math` without colliding. The owning project is part of the id, and renaming a project does not change ids it already published. Download a published environment, or one file from it, to local disk. `ENV_ID` must be a managed Freesolo hub slug `your-org/your-project/your-env`. Without `PATH`, the whole environment is written to a directory. With `PATH`, only that file is fetched. `-o` sets the output path; `-f` overwrites an existing output. List your organization's published environments alongside the local sources you can publish. Each published id is ready to paste into `[environment] id`. If you are not logged in, the local sources still list and the published section says so rather than reporting an empty catalog. Delete a managed Freesolo Hub environment. `ENV_ID` must be a lowercase `namespace/project/name` Hub id; GitHub refs and local paths cannot be deleted from the Hub. `--project` is required and must be the UUID of the project that owns the environment. Pass `-y`/`--yes` to skip the confirmation prompt. ## Traces Export a project's [recorded traces](/guides/tracing) as freesolo environment records, ready to train on. By default each trace becomes an `{"input", "output"}` row, the same shape `flash env setup` scaffolds, so the output drops straight into an environment's `dataset/train.jsonl`. A file exported here matches the dashboard's trace export. `--format` picks the export shape (requires Flash 1.0.22; the dashboard export offers the same three): * `records` (default): `{"input", "output"}` environment records. Traces with no usable request/response pair are skipped. * `prompts`: `{"input"}` only. GRPO and OPD both train from prompts alone (GRPO samples its own completions and scores them with the environment; OPD distils from a managed teacher), so no gold reply is needed, and a call whose reply never arrived still exports. * `raw`: the stored trace rows with their spans, unconverted. Raw rows are not a dataset, so they default to `traces.raw.jsonl` instead of `dataset/train.jsonl`, where a later `env push` + `train` could pick them up. There is no per-algorithm format: every algorithm reads the same environment records. Traces are stored per-project, so an export always reads exactly one project. Without `--project`, an interactive terminal prompts you to pick from the projects your key can reach; a non-interactive terminal errors with the available ids so you can pass `--project`. An export reads the newest 1000 traces; skipped traces are counted in the summary. Writing an existing file needs `--force`. | Flag | Default | Effect | | ---------------- | ---------------------------------------------- | --------------------------------- | | `--project ID` | prompt | Project id to export from | | `--format NAME` | `records` | `records`, `prompts`, or `raw` | | `-o`, `--output` | `dataset/train.jsonl` (`traces.raw.jsonl` raw) | Output JSONL path | | `-f`, `--force` | - | Overwrite an existing output file | ## Training Submit a managed training run from a [TOML config](/reference/configuration) and follow its logs. The config must set a top-level `project` UUID; Flash validates it against your organization before any GPU is allocated, so a missing, malformed, or foreign project fails the submit rather than starting paid work. Before submit, Flash checks your prepaid balance against the pre-flight estimate. Successful runs are billed at the quoted Flash cost. See [Cost and billing](/reference/cost-model). | Flag | Effect | | ----------------- | --------------------------------------------------------------------- | | `--dry-run` | Run submit checks without paid training or a training GPU | | `--cost` | Print the pre-flight USD cost without paid training or a training GPU | | `--background` | Submit and return immediately instead of following logs | | `--gpus N` | Most cards to run the job on (`1`-`8`); sugar for `--set gpu.count=N` | | `--set KEY=VALUE` | Override a config value (dotted key, repeatable) | | `--config FILE` | Deep-merge additional TOML for config composition (repeatable) | SFT estimates return directly from the selected packaged dataset without starting paid training or allocating a training GPU. An unreadable package fails before allocation. See [Cost and billing](/reference/cost-model#grpo-and-opd-quote-locally-sft-reads-packaged-data). On an OPD config, `--dry-run` preflights the managed teacher before allocation. It also checks image rows visible in packaged datasets and inline `records` before allocation; images created dynamically by environment code can fail only at worker time, after allocation. `--gpus` pins a ceiling, not an exact count: allocation still picks a single card when one fits, and only rentable shapes (1, 2, 4, 8) are provisioned. It overrides a `[gpu] count` in the config, and omitting it leaves an authored count untouched. See [Multi-GPU runs](/reference/configuration#multi-gpu-runs) for the auto-sizing rule. ## Run management List your runs with state, algorithm, cost, and model. Print a run's status, including its current or final cost record. On a styled terminal the default is a status panel; redirected output and `--json` use the complete machine-readable object. `-f`/`--follow` polls until the run reaches a terminal state without replaying logs and prints only changed statuses. With `--json`, follow mode emits one compact JSON object per line, producing a JSONL stream suitable for line-by-line processing. Print a run's full console and error logs. `-f`/`--follow` streams new logs until the run reaches a terminal state. Cancel a run. The CLI waits for the run to stop before returning, which can take several minutes. List a run's saved SFT, GRPO, or OPD checkpoints available to deploy. Serve one with `flash models deploy RUN_ID/step-N`. When output is not a styled terminal, each line is single-space separated (`step N RUN_ID/step-N`), so it splits cleanly in scripts (`awk`, `grep "step N"`). ## Serving Deploy a final adapter or checkpoint. Every real deploy resolves an immutable revision, runs a mandatory bounded smoke, and activates the stable run-id alias only after verification. `--dry-run` previews without creating a deployment. Serving is [billed per token](/guides/deploy-and-chat#billing). If no final adapter exists, use the `RUN_ID/step-N` selector listed by `flash runs checkpoint RUN_ID`. Without `--wait`, deploy returns while the revision is still queued. With it, the command blocks until the revision is servable and exits `0`, or exits `1` if the deployment failed, the wait timed out, or the alias rolled back to the previously deployed revision. The timeout defaults to 2400 seconds; pass a value (`--wait 600`) to set your own, or `--wait 0` for a single state read. Interrupting the wait with `Ctrl-C` stops waiting, not the deployment. Send a message to a deployment. `TARGET` is the stable `RUN_ID` alias, a full immutable revision, or `RUN_ID/step-N` for a checkpoint you have already deployed, resolving to that checkpoint's verified revision, so run `flash models deploy RUN_ID/step-N` first. | Flag | Default | Effect | | ----------------- | ---------- | --------------------------------------------------------------------------------------------------------------------- | | `-m`, `--message` | (required) | The user message | | `--system` | - | Optional system prompt sent ahead of the user message. It is transient and useful for training-prompt parity in evals | | `--max-tokens` | `512` | Max tokens to generate | | `--temperature` | `0.0` | Sampling temperature | List each active run alias and its currently active verified revision. Human output shows run id, step, revision, state, verification time, OpenAI model, and detail. `--json` includes complete records and `openai_base_url`. Disable the stable alias and all immutable revisions for the run. ## Serving in your own account `flash serve` provisions serving in **your own** Modal or RunPod account, instead of Freesolo's managed serving. The GPU runs and bills in your account, and the endpoint is a provider URL you call directly. See [Serve in your own account](/guides/deploy-and-chat#serve-in-your-own-account). These are separate from `flash models deploy`/`undeploy`, which drive managed serving. The two do not mix: a customer-owned deployment does not accept `FREESOLO_INTERNAL_KEY` and has no adapter-registration surface for `flash models deploy` to drive. `flash serve` needs the `server` extra (`pip install 'freesolo-flash[server]'`), because it resolves the adapter through Hugging Face and drives the provider SDK. Credentials are **request-only**: they are read from the environment for the duration of one call and are never stored, logged, or written into the deployment record - so each command needs them exported again. | Variable | For | | -------------------------------------- | -------------------------------------------------------------------------------------------------- | | `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET` | `--provider modal` (`modal token new` writes them) | | `RUNPOD_API_KEY` | `--provider runpod` | | `FLASH_SERVING_KEY` | The endpoint's own bearer key; generate it yourself | | `HF_TOKEN` | Lets the container hydrate its adapter; required for a private artifact repo, which is the default | Provision one deployment in your own account for one base model and one run's adapter. Prints the endpoint URL, the provider resource ids, and an `identity` string that addresses the same deployment later. | Flag | Default | Effect | | ---------------------- | --------- | --------------------------------------------------------------------- | | `--provider` | required | `modal` or `runpod` | | `--model` | required | Base model id to serve | | `--run` | required | Run id whose adapter to serve | | `--deployment-id` | required | Stable id for this deployment across generations | | `--generation` | `1` | Generation number for this deployment | | `--image` | required | Digest-qualified serving image (`name@sha256:...`); a tag is rejected | | `--artifact-repo` | required | Hub repo holding the adapter | | `--artifact-subfolder` | required | Path within that repo | | `--artifact-repo-type` | `dataset` | Repo type; Flash runs publish adapters as datasets | | `--lora-rank` | required | The adapter's LoRA rank | | `--checkpoint-step N` | final | Serve one saved step instead of the final adapter | | `--thinking` | off | Default this adapter to thinking mode | | `--timeout SECONDS` | `3600` | How long to wait for the provider operation | | `--dry-run` | - | Resolve and validate every input without contacting the provider | Placement flags are per provider, and exactly one provider's set is required even though `--help` lists them all as optional. Passing the other provider's flags is rejected rather than ignored. | Provider | Required placement | Optional | | -------- | ------------------------------------------------------------ | ------------------------------------------------- | | `modal` | `--modal-workspace`, `--modal-environment`, `--modal-region` | `--modal-web-suffix` (if the environment has one) | | `runpod` | `--runpod-account`, `--runpod-data-center` | - | A broad Modal region (`us-east`) is cheaper and draws on a larger capacity pool than a narrow one (`us-east-1`). Show the proved state of one deployment. Read-only: it never mutates provider resources and never reports an unproved deployment as ready or absent. Takes the same identity flags as `deploy`, or pass `--deployment-identity` with the `identity` string `deploy` printed to skip Hub resolution. Exits `0` for `ready`, `provisioning`, or a confirmed-`absent` deployment, and `1` when no state could be proved. Tear down one deployment generation and prove its resources are gone. Takes the same identity flags as `deploy`, plus the provider resource ids that `deploy` printed (`--modal-app-id`, `--modal-volume-id`, `--modal-inference-secret-id`, or `--runpod-pod-id`, `--runpod-network-volume-id`, `--runpod-template-id`, `--runpod-inference-secret-id`), so every deletion binds to the exact generation. For a RunPod create whose outcome was never confirmed, omit all the RunPod ids and pass `--deployment-identity` to reclaim by identity instead. If `deploy` reports `provisioning`, `outcome_unknown`, or an artifact-cleanup failure, provider resources may be live and billing. Run `flash serve status` to inspect, then `flash serve undeploy` to stop them - do not retry the deploy, which would provision and bill twice. ## Export Export a trained adapter to a HuggingFace repo you own (created if it doesn't exist). `--adapter-id` is either `RUN_ID` for the final adapter or `RUN_ID/step-N` for a saved checkpoint. `--repository` is required. | Flag | Default | Effect | | ----------- | ---------- | ------------------------------------------------------------------------------------------------------------- | | `--api-key` | `HF_TOKEN` | HuggingFace token with write access to `--repository` (read from your shell or a local `.env` / `.env.local`) | | `--public` | private | Create the destination repo as public | # Configuration reference Source: https://docs.freesolo.co/reference/configuration Every field in a Flash training config (TOML). Describe a run in a TOML config, then pass it to `flash train`. Every run requires `project`, `model`, and `environment.id`. `algorithm` defaults to `sft`. `flash train config.toml --dry-run` runs submit-time checks without starting paid training or allocating a training GPU. See [Cost and billing](/reference/cost-model) for estimate behavior. ```toml theme={null} project = "00000000-0000-0000-0000-000000000000" model = "Qwen/Qwen3.5-4B" algorithm = "sft" seed = 42 [environment] id = "your-org/your-project/your-env" [train] epochs = 3 max_examples = 1000 lora_rank = 32 ``` ## Top level | Key | Type | Default | Description | | ----------- | ------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `project` | string | (required) | Freesolo project UUID that the run belongs to. Validated against your organization at submit time, before any GPU is allocated. Create one with [`flash projects create`](/reference/cli#projects). | | `model` | string | (required) | Base model to train a LoRA adapter on, e.g. `Qwen/Qwen3.5-4B`. See [`flash models list`](/reference/models). | | `algorithm` | string | `sft` | `sft` imitates completions, `grpo` learns from your reward, and `opd` distils from a managed teacher. | | `seed` | int | `42` | Deterministic run seed for training order and sampling. Must be between `0` and `9223372036854775807`. | | `thinking` | bool | `false` | Enable reasoning mode on a thinking-capable model. Reasoning and the answer share `max_completion_tokens`. | ## `[environment]` | Key | Type | Default | Description | | --------- | ------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | (required) | Environment source accepted by the control plane. Managed Freesolo requires a published `namespace/project/name` slug. A standalone plane requires a `github:` reference or supported GitHub repository, tree, or blob URL that resolves to `environment.py`, and rejects managed slugs. Local paths are not valid training sources. | | `params` | table | `{}` | Keyword arguments passed to the env's `load_environment(**kwargs)`. Flash also honors `split` for packaged env datasets. See [Datasets](/guides/datasets#load-sidecars): it selects the first of `dataset/.jsonl`, `dataset/.json`, `.jsonl`, or `.json` that exists, and a missing split file is an error (no silent `train.jsonl` fallback). | | `pip` | list\[string] | `[]` | Third-party requirements your scorer imports, installed on the worker alongside the managed runtime. See [Scorer dependencies](#scorer-dependencies). | | `secrets` | list\[string] | `[]` | Environment variable names to make available to your environment at runtime. Values are read from your shell, `.env`, or `.env.local` at submit time. | ```toml theme={null} [environment] id = "your-org/your-project/your-env" secrets = ["SERVICE_API_KEY"] ``` Environment code reads the secret normally: ```python theme={null} import os service_api_key = os.environ["SERVICE_API_KEY"] ``` Never put secret values in `[environment.params]`; those values are recorded on the run. Every name you list under `[environment].secrets` must resolve to a value in your shell, `.env`, or `.env.local` when you submit. Names such as `FREESOLO_API_KEY`, `RUN_ID`, and `GITHUB_TOKEN` are reserved by Flash. ### Scorer dependencies If your reward function imports a third-party package the managed runtime does not ship, declare it under `[environment] pip` and the worker installs it before your environment loads: ```toml theme={null} [environment] id = "your-org/your-project/your-env" pip = ["pymongo==4.10.1", "rapidfuzz>=3.9"] ``` Each entry is a requirement string, so the list form is required - `pip = "pymongo"` is rejected with the bracketed form to use. Entry **syntax** is validated at submit, before a GPU is allocated, because a malformed entry would otherwise fail mid-install on hardware you are already paying for: * Only requirements, never pip options. A leading `-` (`--no-deps`, `--target=...`, `--extra-index-url=...`) is rejected: those change how the mandatory worker requirement installs. * No credentials in a URL. The spec is stored and uploaded in plaintext, so a token in a direct or VCS requirement URL would be written to the run record. Any userinfo before the host is rejected, as is any query string. Private requirements that need authentication to install are therefore not supported: `[environment] secrets` supplies values to your environment at runtime, after installation, so it cannot authenticate pip. Vendor the package into the folder you publish, or depend on a publicly installable one. Your requirements install alongside Flash's own managed worker requirement rather than replacing it, so the runtime your environment loads into is never displaced. A transient package-index failure is retried before the run is given up on; a genuine resolution or build failure is terminal, because retrying it would only burn GPU time. A dependency you forgot to declare otherwise surfaces on the worker as a scorer that cannot import. `flash env test` runs against your local interpreter, so a package installed on your machine but missing from `pip` passes locally and fails only once the run starts - declare everything your scorer imports, rather than relying on a local pass. ## `[train]` `[train]` is one flat table shared by all three algorithms. Keys that apply everywhere are below; the rest are **scoped by algorithm** and a knob the run's algorithm cannot consume is rejected at submit rather than silently ignored. See [Knobs are scoped by algorithm](#knobs-are-scoped-by-algorithm). | Key | Type | Default | Description | | -------------------- | ---------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `lora_rank` | int | `32` | LoRA rank; higher means a larger adapter with more capacity and cost. Flash enforces the per-model max rank. Omit with `init_from_adapter`; the source rank is authoritative. See [context window and LoRA rank](/reference/models#context-window-and-lora-rank). | | `lora_alpha` | int | `2 * lora_rank` | LoRA scaling factor. Omit it for the tuned `2 * lora_rank` default; set it to depart from that ratio. Rejected alongside `init_from_adapter`, where the source adapter's alpha is authoritative. Requires Flash 1.1.35. | | `learning_rate` | float | recipe default | Optimizer learning rate. | | `batch_size` | int | recipe default | **SFT only.** Examples per optimizer update. GRPO and OPD reject it - see [The optimizer batch has a different name per algorithm](#the-optimizer-batch-has-a-different-name-per-algorithm). | | `prompts_per_step` | int | `64` / `8` | **GRPO and OPD only.** Prompts drawn per optimizer update. Defaults to `64` for GRPO and `8` for OPD. SFT rejects it. Requires Flash 1.1.43. | | `max_context_tokens` | int | recipe default | Total training context cap. SFT applies it to the rendered sequence; GRPO and OPD apply it to prompt plus completion, and must leave room for a prompt once the completion budget is reserved. It cannot exceed the model's training context cap. See [context window](/reference/models#context-window-and-lora-rank). | | `epochs` | int | recipe default | Passes over the retained rows or prompt pool when `max_steps` is not positive. | | `max_examples` | int | see below | Rows or prompts retained from the dataset. Omitted or `0` means no cap. SFT keeps the first N in file order before the seeded shuffle; GRPO and OPD truncate the prompt pool, and need this or `max_steps` to be quotable - see [GRPO and OPD need a stated horizon](#grpo-and-opd-need-a-stated-horizon). | | `max_steps` | int | derived | A positive value is the exact update horizon for SFT, GRPO, and OPD. Absent or non-positive uses the derived count, except on GRPO and OPD with no `max_examples` - see [GRPO and OPD need a stated horizon](#grpo-and-opd-need-a-stated-horizon). | | `save_at_steps` | list\[int] | `[]` | Strictly increasing positive steps to save a deployable checkpoint at. Requires positive `max_steps`, cannot exceed it, and suppresses `save_every`. | | `save_every` | int | recipe default | Checkpoint cadence when `save_at_steps` is empty. | | `init_from_adapter` | string | - | Continue any finished adapter from `RUN_ID` or the exact `RUN_ID/step-N` shown by `flash runs checkpoint`. Longer refs are rejected. Omit `lora_rank` and `lora_alpha`; the source adapter's values are authoritative. See [Warm-start from any algorithm](#warm-start-from-any-algorithm). | `--dry-run` resolves warm-start rank, alpha, and the source base-model identity for you. Local `--cost` may warn and estimate with defaults instead. ### Warm-start from any algorithm `init_from_adapter` works for **every** source/target pair. SFT, GRPO, and OPD each read a source adapter produced by any of the three, including same-algorithm continuation - `sft` → `sft` to keep training on more data, or `grpo` → `grpo` to extend a run. SFT was previously rejected as a warm-start target; it is now accepted like the other two. ```toml theme={null} algorithm = "sft" # or "grpo", or "opd" - any of the three [train] # the source run id, from any algorithm. add /step-N to continue a specific # checkpoint listed by `flash runs checkpoint `. init_from_adapter = "" ``` Across the lineage keep the **same base model**. The source adapter's rank and alpha are authoritative, so setting `lora_rank` or `lora_alpha` alongside `init_from_adapter` is rejected at submit, and a source rank above the model's serving cap is rejected too. A warm-started SFT run also **inherits its source's base-model pin** rather than resolving its own, so the continued adapter stays deployable when the base model's upstream tip moves. ### Knobs are scoped by algorithm There are no `[sft]`, `[grpo]`, or `[opd]` tables: every knob lives under `[train]`, and the error names the key you set. `--dry-run` surfaces these without allocating a GPU. | Knob | SFT | GRPO | OPD | | --------------------------------------------------------------------------------------------------------------- | -------- | -------- | -------- | | `group_size`, `temperature`, `max_completion_tokens`, `kl_penalty_coef`, `stop_sequences`, `structured_outputs` | rejected | yes | yes | | `entropy_quantile`, `thinking_length_penalty_coef`, `credit_assignment` | rejected | yes | rejected | | `teacher_model` | rejected | rejected | yes | | `batch_size` | yes | rejected | rejected | | `prompts_per_step` | rejected | yes | yes | ### GRPO and OPD need a stated horizon SFT estimates its packaged dataset, so an uncapped SFT config prices fine. GRPO and OPD quote offline from the catalog with nothing reading the prompt pool, so a config that sets neither `max_examples` nor a positive `max_steps` is **refused rather than quoted**: ```text theme={null} cannot price grpo without a prompt-pool size: set [train] max_examples to the row count the run will train on, or [train] max_steps to state the horizon directly ``` Set either one. `[environment.params] max_examples` does not count: it reaches your environment as an opaque `load_environment()` kwarg that the starter templates ignore, so only `[train] max_examples` is applied by the worker. ### The optimizer batch has a different name per algorithm SFT authors `batch_size`; GRPO and OPD author `prompts_per_step`. They are not interchangeable, so each is rejected under the other algorithm, naming the key you meant: ```text theme={null} [train] batch_size does not apply to grpo: use prompts_per_step instead. batch_size is sft-only, and the two are different quantities -- prompts_per_step is the optimizer batch itself, so copying an sft batch_size here would change how many prompts each update trains on. ``` Under SFT, `batch_size` is examples per optimizer update. Under GRPO and OPD, `prompts_per_step` is prompts per optimizer update, and each step samples `prompts_per_step x group_size` completions. Do not carry a value across when porting a config: a typical SFT `batch_size` is far smaller than the `64` GRPO and `8` OPD defaults. ### SFT-specific Its only algorithm-only key is `batch_size` - every rollout knob is rejected, because SFT trains on your dataset's completions and never generates. #### When sequence packing is off Flash packs examples when it can do so safely; multimodal rows are never packed. `--cost`, `--dry-run`, and `train` warn when packing is off. An unpacked run trains **exactly one example per optimizer update**, regardless of a larger authored `batch_size`. That value can still affect the quote and GPU choice. The default learning rate assumes a batched update, so expect noisier steps and consider a lower `learning_rate` than for a packed run. ### GRPO-specific | Key | Type | Description | | ------------------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompts_per_step` | int | Prompts per optimizer update (default `64`). Each step samples `prompts_per_step x group_size` completions. SFT's `batch_size` is rejected here. See [the rollout shape](#the-grpo-rollout-shape-is-a-fixed-set). | | `group_size` | int | Completions sampled per prompt. Exactly `2`, `4`, or `8`; defaults to `8`. See [the rollout shape](#the-grpo-rollout-shape-is-a-fixed-set). | | `temperature` | float | Sampling temperature for rollouts. | | `max_completion_tokens` | int | Maximum generated tokens per completion or assistant turn. | | `kl_penalty_coef` | float | Strength of the penalty that limits drift from the base model. | | `entropy_quantile` | float | Restrict the GRPO policy update to the highest-entropy fraction of completion tokens. Between `0` and `1`; the default (unset) trains on all tokens, while a smaller value (for example `0.2`) trains on only the top-entropy tokens. | | `thinking_length_penalty_coef` | float | Penalty on reasoning length. | | `stop_sequences` | list\[string] | Stop sequences for generation. | | `structured_outputs` | string/table | Constrain each rollout to a JSON schema, regex, choice set, or any JSON. See [Structured outputs](/guides/structured-outputs). | | `credit_assignment` | string | `per_episode` (default) scores one reward for the whole rollout; `per_turn` gives each assistant turn its own group-relative advantage in pure multi-turn GRPO. Tool-calling multi-turn environments require `per_episode`, and single-turn runs are equivalent either way. See [per-turn credit](/guides/environments/multi-turn#per-turn-credit-grpo). | #### The GRPO rollout shape is a fixed set GRPO `group_size` accepts exactly **`2`, `4`, or `8`**, and defaults to `8`. Any other value - including `1`, `3`, `6`, or anything above `8` - is rejected at submit, before a GPU is looked up or allocated. Flash never rewrites the value you authored, so an unsupported `group_size` fails fast instead of quietly training a different shape than you asked for. A step also has a completion ceiling: ```text theme={null} train.prompts_per_step * train.group_size must be <= 512 for GRPO (got 128 * 8 = 1024) ``` The default `64 x 8` is exactly `512`. Admission checks the authored values, or their defaults when omitted, so an oversized shape is refused up front - a smaller retained dataset cannot rescue it later. To raise one factor, lower the other. At execution the **effective prompt count** may clamp down to the number of valid prompts left after `max_examples` and prompt-budget filtering; `group_size` is never changed. Rollout generation, reward work, and cost all scale with the effective `prompts_per_step x group_size` product. These limits are GRPO's. OPD keeps its own `group_size` default of `1` and is not restricted to the same set. ### OPD-specific OPD derives updates from epochs, retained prompts, and `prompts_per_step` unless positive `max_steps` sets an exact horizon; one of those or `max_examples` is [required](#grpo-and-opd-need-a-stated-horizon). An SFT warm-start is recommended. | Key | Type | Default | Description | | ----------------------- | ------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `teacher_model` | string | `glm-5.2` | Managed teacher alias from [Managed teachers](#managed-teachers). Image prompts need an image-capable teacher. Teacher tokens are not billed to you. | | `prompts_per_step` | int | `8` | Prompts per optimizer update. Rollout concurrency is `prompts_per_step x group_size`. SFT's `batch_size` is rejected here. | | `group_size` | int | `1` | Completions sampled per prompt. | | `temperature` | float | `1.0` | Student sampling temperature. | | `max_completion_tokens` | int | `512` (`1536` with `thinking`) | Maximum generated tokens per completion or assistant turn. | | `kl_penalty_coef` | float | `1.0` | Reverse-KL strength. Must be greater than `0`. | | `stop_sequences` | list\[string] | - | Stop sequences for student completions. | | `structured_outputs` | string/table | - | Guided decoding for each student rollout. | There is no auxiliary EOS loss. For termination problems, use an SFT warm-start, verify teacher quality, set a hard prompt budget, add suitable `stop_sequences`, and inspect `truncated_rollouts`. The local `--cost` estimate may itemize teacher usage for diagnosis, but only GPU time is charged. #### Managed teachers The teacher is platform-managed: `teacher_model` takes one of these friendly aliases, and a provider or repository identifier is never accepted. An unsupported value is rejected before a GPU is provisioned. | Alias | Teacher | Sees images | | ------------------- | ------------------ | ----------- | | `glm-5.2` | GLM 5.2 (default) | no | | `kimi-k3` | Kimi K3 | no | | `deepseek-v4-pro` | DeepSeek V4 Pro | no | | `qwen3.5-397b-a17b` | Qwen3.5 397B A17B | yes | | `qwen3-vl-235b` | Qwen3-VL 235B A22B | yes | #### Image-bearing OPD Distilling a prompt that contains an image requires a teacher that can see it. Select one of the image-capable aliases above; any other teacher is rejected, with the aliases you can use named in the error. When the images are in your dataset file, Flash sees them at submit and rejects a text-only teacher **before a GPU is allocated**. When your environment code attaches the image instead - building it inside `build_prompt_messages`, say - nothing can see it until that code runs, so the same rejection happens on the worker after allocation. Both fail the run rather than distilling blind. Multi-turn image-bearing OPD is not supported. Flash rejects statically visible image-bearing multi-turn inputs before allocation, while images attached later by environment execution are rejected by the worker guard. Single-turn image prompts are the supported shape. Requires Flash 1.1.42. ### Structured outputs `structured_outputs` constrains GRPO and OPD rollouts; SFT rejects it. Set exactly one JSON schema, `regex`, `choice`, or `json_object` constraint. Optional whitespace controls are documented in the guide. Combining constraints or using unsupported grammar forms is rejected. With `thinking = true`, reasoning remains free-form and the grammar begins after ``. See [Structured outputs](/guides/structured-outputs). ## `[gpu]` | Key | Type | Default | Description | | ----------- | ----------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string or list\[string] | omitted | A string hard-pins one active validated class from `flash gpus`. A non-empty list names acceptable classes; list order is not priority because quoting and allocation cost-rank the whole set. Every entry must be supported, provider-compatible, and large enough. Omit it for managed cheapest-fit allocation. | | `count` | int | auto-size | Omit this **and** `type` to let Flash pick the smallest shape that fits. Authoring either form of `type` without `count` is a single-card constraint. Set `count` to pin a ceiling of `1`-`8` cards. See [Multi-GPU runs](#multi-gpu-runs). | | `provider` | string | omitted | Hard-pin one GPU provider: `runpod`, `lambda`, or `vast`. The provider must be able to provision every acceptable `type`. Cannot be combined with `providers`. | | `providers` | list\[string] | omitted | Ordered soft provider preference. When authored, the list must be non-empty. Named configured providers are preferred in order, but configured providers omitted from the list remain eligible. Use `provider` instead when other providers must be excluded. Cannot be combined with `provider`. | The authored class constraint and provider routing are honored in estimates, submissions, and retries. Use a scalar `type` or `provider` when hardware or provider identity is mandatory; use lists when several outcomes are acceptable. ```toml theme={null} [gpu] type = ["A100 PCIe", "A100 SXM"] providers = ["lambda", "runpod"] ``` This allows either GPU class, prefers Lambda and then RunPod, and still permits another configured provider if neither preference can place the run. Replace `providers` with `provider = "lambda"` to make Lambda a hard requirement. ### Multi-GPU runs **Omit both `type` and `count` and Flash sizes the run for you.** It picks the smallest geometry-safe card count your configuration actually fits on, then ranks the fitting shapes by cost per step. A run that needs more memory than one card holds is placed on two, four, or eight instead of being rejected. Only rentable shapes are ever provisioned: `1`, `2`, `4`, and `8`. Setting `count` turns it into a **hard ceiling that never escalates**: ```toml theme={null} [gpu] count = 4 # at most 4 cards; the allocator may still choose 1 or 2 ``` A ceiling of `3` therefore never buys 4 cards - it permits 1 or 2. An explicit `count = 1` on a run that does not fit one card is an error rather than a silent escalation, and it names the smallest count that would work: ```text theme={null} grpo needs >= 229 GB VRAM; gpu.count=1 provides at most 180 GB (B200). Raise the card ceiling with `--gpus 2` (2x H200 = 234.1 GB), or lower [train].max_context_tokens / [train].max_completion_tokens / [train].lora_rank to fit. ``` If nothing fits even at eight cards, the error names the knobs that actually shrink that algorithm - they differ per algorithm. See [Pre-flight says the run is too large](/reference/troubleshooting#run-fit-and-resource-use). Authoring `type` without a `count`, whether as a scalar pin or an acceptable list, stays a **single-card** constraint. Auto-sizing across multiple cards applies only when you omit both keys. `flash train --gpus N` sets the same ceiling from the command line without editing the config. Cost is quoted for the shape the run actually occupies, so a multi-card run is priced on every card it held. A run's VRAM requirement is a **whole-run** figure, and a multi-card shape pools its cards to meet it. The quote spells out both numbers so a shape that fits does not read as a rejection: ```text theme={null} GPU : 2x B200 (300 GB usable across 2x 180 GB; run needs >= 199 GB) @ $5.89/hr per card ``` The per-card size stays visible because that is what you check against a provider listing. If an authored single-card pin cannot hold the run, the error now names the card count that would - for example `it fits on 2 cards -- raise the card ceiling with --gpus 2` - so a run needing more than the largest single card is not a dead end. ## `[wandb]` Optional [Weights & Biases](https://wandb.ai) logging labels. These values are non-secret; set `WANDB_API_KEY` in your local environment when submitting a run to enable logging to your own W\&B account. | Key | Type | Description | | ---------- | ------ | ------------------ | | `project` | string | W\&B project name. | | `run_name` | string | W\&B run name. | ## Overrides & composition Set any value at submit time without editing the file. The override flag is `--set` (repeatable, dotted keys): ```bash theme={null} flash train config.toml --set train.epochs=3 --set train.lora_rank=16 flash train base.toml --config overlay.toml # deep-merge extra TOML flash train config.toml --gpus 4 # sugar for --set gpu.count=4 ``` The config file is the base, `--config` overlays merge onto it, and `--set`/`--gpus` win over both, applied left to right. An absent `--gpus` is indistinguishable from no flag, so a `[gpu] count` authored in the file is never silently downgraded. # Cost and billing Source: https://docs.freesolo.co/reference/cost-model How to preview Flash training cost, what affects it, and how charges are applied. `flash train --cost` prints a pre-flight estimate before you submit a run: ```bash theme={null} flash train config.toml --cost ``` Use it before any non-trivial run: it validates the config and prices the training work without starting paid training or allocating a training GPU. GRPO and OPD quote locally from the catalog. SFT returns a bounded static estimate from the pinned environment package in the same command. On an SFT config, omitting `max_examples` (or setting it to `0`) trains on every row. The submit-time quote is the amount Flash checks against your prepaid org balance. Successful runs are billed at the quoted Flash cost. A cancelled run is prorated from that quote by the share of the work it completed, capped at the quote - see [Charges and cancellations](#charges-and-cancellations). ## GRPO and OPD quote locally; SFT reads packaged data **GRPO and OPD** quote offline from the catalog, so `--cost` returns immediately without contacting the server. They need a stated horizon to price: set `[train] max_steps` for an exact update count, or `[train] max_examples` for the prompt-pool size epochs run over. A config that states neither is refused rather than quoted: ```text theme={null} cannot price grpo without a prompt-pool size: set [train] max_examples to the row count the run will train on, or [train] max_steps to state the horizon directly ``` Because it stays local, that quote checks only local things: the TOML, the algorithm's knobs, the catalog, the resource fit, the horizon, and the price. It cannot tell you whether your project and environment are reachable, whether your balance covers the run, or whether the teacher is available. Run `--dry-run` for those before a real submit. **SFT uses an authenticated static estimate from the pinned environment package.** Flash reads packaged JSON or JSONL rows, uses their raw `input` and `output` fields, and applies the statically readable training contract. Contract lookup follows `contract_text`, then `contract_path`, then `TRAINING_CONTRACT.md`. * No paid training starts, no training GPU is allocated, and there is no training charge or separate profile job. * Flash does not execute `environment.py`, `load_environment()`, dataset hooks, prompt construction, filtering, or scorer code. * A missing, empty, invalid, unreadable, or oversized dataset fails before GPU allocation. The limit is 32 MiB for the selected packaged dataset or inline `records`, and 256 KiB for the training contract. * The estimate returns directly in the command, with nothing else to follow. The estimate is intentionally conservative about what it can know. Environment code may add prompts, few-shot examples, tool schemas, filtering, or other transformations that the static read cannot execute. Actual training may retain fewer rows, truncate more content, or run fewer effective steps than this static workload estimate predicts. The accepted submit-time quote remains the billing boundary: successful runs bill at that quote, while cancellations are prorated and capped at it. The SFT output therefore labels the workload values as packaged-dataset estimates rather than exact measurements. ### The counts come from the published environment, not your working copy An SFT estimate reads the **published** environment package your config points at, so `--cost` and `--dry-run` name the environment id and commit the counts came from: ```text theme={null} env : published environment your-org/your-project/your-env a1b2c3d4e5f6 (published commit) ``` Local edits you have not pushed are not in that estimate. If the numbers look stale, run `flash env push --name NAME --project PROJECT_UUID [path]` again to republish, then re-estimate - the output says so when the counts came from a published managed or GitHub environment. A GitHub id pinned to an exact commit will not pick up later changes at all until you move the pin. Inline `[environment.params] records` are different: those rows come from your config, so the package supplied the environment but not the dataset, and the output labels them accordingly. ## How cost is calculated `flash train --cost` reprices a run the same way the platform bills it: ```text theme={null} total = billable training hours × GPU $/hr × cards occupied ``` The rate is **per card**, so a multi-card run is priced for every card it held. A single-card run occupies one and the last term drops out. The preview separates **Setup**, **Train**, and **Billable** time. Setup is not billed. Train covers the estimated training work, and Billable is the time used for the charge, including required finalization. Work before the first optimizer metric can therefore still be billable. Per-step time reflects user-selected work: GRPO scales with completion count and length (`prompts_per_step × group_size × max_completion_tokens`); SFT scales with the estimated training tokens; and OPD samples student completions before the managed teacher grades them. Teacher tokens are not billed to you. The estimator prices the cheapest eligible GPU automatically. A scalar `[gpu] type` pins one class; a list names acceptable classes that the estimator and allocator cost-rank together. The same constraint persists across submissions and retries. When you omit both `[gpu] type` and `[gpu] count`, the quote prices the shape Flash will actually auto-size to: the smallest geometry-safe card count your configuration fits on, ranked by cost per step. A configuration that needs more memory than one card holds is therefore quoted and billed as a multi-card run rather than rejected. Set `[gpu] count` (or `--gpus N`) to cap that. Authoring `[gpu] type`, as either a scalar or a list, without a count stays a single-card constraint, so an oversized run is rejected at preflight rather than quoted across several. See [Multi-GPU runs](/reference/configuration#multi-gpu-runs). Serving is priced separately; see [Serving billing](#serving-billing). ## What affects training cost You control the main levers in the config: * **[Base model](/reference/models).** Smaller models are cheaper and faster for smoke tests. * **[Algorithm](/guides/training#choose-a-training-algorithm).** SFT usually costs least. GRPO and OPD both sample and score the model's own completions before each update, so they cost more per step. Every algorithm is billed on GPU time; OPD's managed teacher is not billed to you. * **Epochs and exact step horizons.** More `epochs` increases cost when Flash derives the update count. For SFT, GRPO, and OPD, positive `max_steps` sets the exact optimizer-update horizon; GRPO and OPD need that or `max_examples` to be quotable at all. * **Required saves.** Each `save_at_steps` entry adds required finalization work, so save cadence is a controllable cost lever. * **Sequence length.** Larger `max_context_tokens` and `max_completion_tokens` increase work per example. Keep them large enough for your prompt and answer, but do not oversize them by default. * **Batch size and dataset size.** For SFT, cost scales with the number of examples trained over and the batch/epoch settings. * **On-policy sample count.** For GRPO and OPD, `group_size` controls how many completions are sampled per prompt. Larger groups cost more. * **Reward latency.** If your GRPO reward calls an external model or service, slow grading can increase wall-clock time. ## Reading the preview A preview includes the model, algorithm, setup estimate, training estimate, billable training time, and total: ```text theme={null} Run : Qwen/Qwen3.5-0.8B [GRPO, 100 steps] Setup : 9.5 min (not billed) Per step : 11.06 s Train : 18.4 min Wall clock : 0.47 h Billable : 0.31 h (training only) TOTAL : $0.21 ``` `Setup` is non-billable preparation. `Train` is the estimated training interval, and `Billable` is the portion used for the charge, including required finalization as well as optimizer updates. Treat the preview as the quote for the config you submit. If you edit the environment, dataset, model, algorithm, or `[train]` settings, run `--cost` again. When `max_steps` is positive, the preview uses it as the exact update count for SFT, GRPO, or OPD. Otherwise the count is derived from epochs, retained examples, and the optimizer batch (`batch_size` for SFT, `prompts_per_step` for GRPO and OPD). ## Charges and cancellations * A run that completes successfully is billed at the submitted quote. * A run cancelled before its first training step is not charged for training. * A run cancelled after training starts is prorated from the quote you accepted, on the basis of the hardware it actually rented, and is **capped at that quote** - a cancellation can never cost more than letting the run finish. * `Setup` is not billed. `Train` and required finalization contribute to the `Billable` amount shown in the accepted quote. * `flash runs status ` shows the current or final cost record. ## Serving billing Serving is billed per token after deployment: `(prompt − cached) × input + completion × output + cached × cached`, at the per-model rates in [Supported models](/reference/models#serving-prices). Prefix caching is automatic; the cached rate applies to a reused prefix. See [Billing](/guides/deploy-and-chat#billing) for how it works. Serving a **base model with no adapter** (a base-model id in the `model` field) is billed the same way, to the org whose API key made the request — not to an adapter owner. Tear down deployments you are done using: ```bash theme={null} flash models undeploy ``` ## Lowering cost Validate on a smaller base model with short smoke tests before scaling `epochs`, `max_steps`, or the retained dataset size, and lower `group_size` and `max_completion_tokens` until the reward or teacher wiring is proven. For SFT, keep a held-out split and stop adding epochs once held-out quality stops improving. `flash runs checkpoint ` lets you deploy a good intermediate checkpoint rather than assuming the final step is best. ## Why a later quote can change The quote can change when you edit the config, publish different environment contents, change the dataset size, or submit after catalog/pricing updates. The authoritative number is the quote returned for the run you submit. # Supported models Source: https://docs.freesolo.co/reference/models The base models Flash can fine-tune and serve, with sizes, reasoning, and token prices. Flash trains a [**LoRA adapter**](/how-flash-works) on a curated catalog of base models. Set the base model with one line in your config, then list the live catalog from the CLI: ```bash theme={null} flash models list ``` ```toml theme={null} model = "Qwen/Qwen3.5-4B" # required — see `flash models list` for the options ``` ## Catalog Every catalog model supports [**SFT, GRPO, and OPD**](/guides/training#choose-a-training-algorithm), accepts **image inputs** for training and serving (see [Image inputs](/guides/datasets#image-inputs)), and is **hybrid-reasoning**: it runs with or without an explicit reasoning step, which you turn on with [`thinking = true`](/reference/configuration#top-level). The catalog is the whole set of trainable models. A `model` outside it is rejected when the config is parsed, before any GPU is rented, and there is no config key that opts into an uncatalogued id. The error lists the supported ids. ### Serving prices Serving is billed per token after deployment, at the **per 1M token** rates below. Cached prompt tokens are prompt tokens served from the automatic prefix cache; see [Billing](/guides/deploy-and-chat#billing) for when it applies. | Model (`model =`) | Max LoRA rank | Prompt / 1M | Completion / 1M | Cached prompt / 1M | | ---------------------- | ------------: | ----------: | --------------: | -----------------: | | `Qwen/Qwen3.5-0.8B` | 128 | \$0.012 | \$0.060 | \$0.0024 | | `Qwen/Qwen3.5-2B` | 128 | \$0.024 | \$0.120 | \$0.0048 | | `Qwen/Qwen3.5-4B` | 128 | \$0.036 | \$0.180 | \$0.0072 | | `Qwen/Qwen3.5-9B` | 128 | \$0.137 | \$0.228 | \$0.0276 | | `Qwen/Qwen3.6-27B` | 64 | \$0.510 | \$3.666 | \$0.1680 | | `Qwen/Qwen3.6-35B-A3B` | 64 | \$0.238 | \$1.518 | \$0.0792 | ## Context window and LoRA rank Every model trains and serves at a **32768-token context** - a LoRA trained longer than it is served would learn positions inference never uses. `flash train` enforces that cap and the per-model max LoRA rank above at submit: a config whose `max_context_tokens` exceeds the cap, whose GRPO/OPD prompt plus `max_completion_tokens` cannot fit, or whose `lora_rank` exceeds the max rank, is rejected. ## Choosing a model Use `Qwen/Qwen3.5-0.8B` or `2B` to validate your setup and data cheaply. Get a run working before you scale. Move to `Qwen/Qwen3.5-4B` or `9B` once the task is wired up and you want stronger results. Larger models cost more per run. Set `model` in your config and submit a run. # Troubleshooting Source: https://docs.freesolo.co/reference/troubleshooting Common Flash errors, why they happen, and how to fix them. Most `flash` errors print one clean line; add the global `--debug` flag before the subcommand (e.g. `flash --debug train config.toml`) for the full traceback. ## Installation & CLI The CLI installs a single `flash` command. Its install location must be on your `PATH`. * If you installed with `uv tool install freesolo-flash`, make sure uv's tool bin directory is on your `PATH` (run `uv tool update-shell`, then restart your shell). * Confirm the install with `flash version`. The CLI is published to PyPI as **`freesolo-flash`**. The bare `flash` name belongs to an unrelated project. Reinstall the right one: ```bash theme={null} uv tool install freesolo-flash ``` ## Authentication Commands that contact Freesolo authenticate with a **Freesolo API key**, verified at login. `flash train --cost` does not start paid training, but on an SFT config it does authenticate. * Create a key in your dashboard at [freesolo.co](https://freesolo.co). * Log in once: `flash login --api-key ` (or set `FREESOLO_API_KEY` instead of passing `--api-key`). * Confirm who the stored key resolves to: `flash whoami`. By default the CLI talks to `https://api.freesolo.co`. To target a different deployment, set `--freesolo-url` (or `FREESOLO_BASE_URL`) at login. See [Auth & identity](/reference/cli#auth-identity). ## Environments The folder you push must contain an `environment.py` file with a `load_environment()` function that returns a Freesolo environment. Then: ```bash theme={null} flash env push --project --name math math ``` It prints the published id (`your-org/your-project/math`) to put in your config's `[environment] id`. The folder needs an `environment.py` at its root and a `--project` UUID your org can reach. A bare `--name` is easiest; if you give a qualified one it must be the full `namespace/project/name`, where the namespace is your org and the project segment matches the `--project` you passed. A two-segment `your-org/math` is no longer a valid name - see [Environments](/reference/cli#environments). Managed training runs already have the Freesolo SDK. Your local Python environment does not get that SDK automatically from the `flash` CLI. Install it locally to run or test `environment.py` directly: ```bash theme={null} uv venv source .venv/bin/activate uv pip install freesolo ``` To pull a published env into your project for local work, use `flash env pull your-org/your-project/your-env`. `flash train --dry-run` checks your config at submit time but never imports `environment.py`, so it cannot prove every worker import resolves. On SFT it reads the packaged dataset and training contract statically; GRPO and OPD do not execute environment code either. If `flash runs log ` shows your `environment.py` failed while importing a package, that package is not available on the worker. The worker installs one managed set - the `freesolo` SDK and the training stack. To add to it, declare the package under `[environment] pip`, which is appended to the managed set rather than replacing it, then submit again. See [Scorer dependencies](/reference/configuration#scorer-dependencies) for the accepted form. If you would rather not add a dependency, rewrite the import against the standard library - `urllib.request` and `json` instead of a vendor HTTP client, for example - or move that logic out of the environment, then republish. See [Dependencies are managed](/guides/environments/package#dependencies-are-managed). Flash also does not install from a `pyproject.toml`, `requirements.txt`, or lockfile bundled with the environment; those describe your local setup only. Pull the specific file you need instead of the whole environment: ```bash theme={null} flash env pull your-org/your-project/your-env environment.py -o environment.py flash env pull your-org/your-project/your-env dataset/eval.jsonl -o eval.jsonl ``` Keep published environments focused on source, small sidecars, and datasets needed by the run. Do not publish virtualenvs, local caches, model weights, or generated artifacts. Use `flash env pull` to inspect the exact packaged file: ```bash theme={null} flash env pull your-org/your-project/your-env dataset/train.jsonl -o train.jsonl ``` For clean A/B experiments, publish changed datasets under a fresh env name so old runs, new runs, and local files are easy to tell apart. SFT estimation happens on the control plane before allocation. It reads the selected packaged JSON or JSONL dataset directly and does not import `environment.py`, so a dataset available only through Python code cannot be estimated. Package a non-empty object-row dataset at the selected split path, such as `dataset/train.jsonl`, or point `[environment.params] dataset_path` at a file inside the package. Fix malformed JSON, missing `input` fields, or paths that escape the package. The control-plane limits are 32 MiB for the selected dataset or inline `records`, and 256 KiB for `contract_text`, `contract_path`, or `TRAINING_CONTRACT.md`. These failures stop before paid training starts or a training GPU is allocated. If your environment module shares a name with an installed Python package, it can shadow or be shadowed by that package. Keep helper module names distinct from installed packages. It depends on the control plane you submit to: * Freesolo's managed service accepts only the published hub slug returned by `flash env push`, for example `your-org/your-project/your-env`. * A standalone plane accepts `github:` references and supported GitHub repository, tree, or blob URLs that resolve to `environment.py`. It rejects managed hub slugs. The two forms do not overlap, and a local file path is not a valid training source on either plane. Use `flash env pull your-org/your-project/your-env` only when you want a local copy of a managed environment to edit or inspect. ## Configuration `model` must be one of the ids in the curated catalog. List the valid ids: ```bash theme={null} flash models list ``` Managed runs train catalog models only. See [Supported models](/reference/models). Flash rejects unknown config sections and `[train]` keys **at parse time**. Check the key against the [configuration reference](/reference/configuration) and validate the config: ```bash theme={null} flash train config.toml --dry-run ``` Dry-run also flags `[train]` keys your CLI version does not recognize, which usually means an outdated CLI. `algorithm` must be `sft` (the default), `grpo`, or `opd`. Fix the value and re-validate with `--dry-run`. Flash rejects a run at submit when its training context is longer than the base model's cap. SFT checks `train.max_context_tokens`; GRPO and OPD check the rollout prompt plus `max_completion_tokens`. Lower those to at or below the cap - see [context window](/reference/models#context-window-and-lora-rank). A GRPO or OPD run reserves `max_completion_tokens` out of `max_context_tokens`, so a context that is not larger than the completion budget leaves nothing for the prompt itself. That is rejected at parse time, before a GPU worker is provisioned: ```text theme={null} [train] max_context_tokens (512) leaves no prompt budget after max_completion_tokens (512) for grpo; set max_context_tokens > max_completion_tokens. ``` Raise `max_context_tokens` above `max_completion_tokens`, leaving enough room for your longest prompt. Note that `thinking = true` raises the default completion budget, so enabling it can trip this on a config that previously parsed. The source adapter is authoritative: omit **both** `train.lora_rank` and `train.lora_alpha` alongside `init_from_adapter`. `--dry-run` resolves the source rank, alpha, and base-model identity automatically. See [Warm-start safely](/guides/training#warm-start-safely). Warm start itself is no longer restricted by algorithm: SFT, GRPO, and OPD can each continue an adapter from any of the three. If an older config avoided a warm-started SFT run because it was rejected, that restriction is gone. GRPO accepts exactly `2`, `4`, or `8`. Any other value - `1`, `3`, `6`, or anything larger - is rejected at submit, before a GPU is allocated. Flash never rewrites the value you wrote, so pick one of the three. A step is also capped at **512 completions**: ```text theme={null} train.prompts_per_step * train.group_size must be <= 512 for GRPO (got 128 * 8 = 1024) ``` Lower one factor to raise the other. This ceiling is checked against the values you authored (or their defaults), so a smaller dataset does not lift it. See [the GRPO rollout shape](/reference/configuration#the-grpo-rollout-shape-is-a-fixed-set). Unknown arguments now come back with the closest real flag for the command you ran, and say when a flag you passed belongs at the root instead of on the subcommand. Take the suggestion, or run `flash --help`. GPU selection is automatic when `[gpu] type` is omitted. A string hard-pins one class; a list names acceptable classes that Flash cost-ranks together. Card **count** is auto-sized only when you omit `type` **and** `count`; `[gpu] count` and `--gpus N` pin a ceiling that never escalates, and either form of `type` without `count` stays a single-card constraint. `[gpu] providers` is an ordered soft preference, not an allowlist, so another configured provider may still win when the preferred providers cannot place the run. Use scalar `[gpu] provider` for a hard pin. `provider` and `providers` cannot be combined. See [GPU configuration](/reference/configuration#gpu). ## Run fit and resource use Expected. GRPO samples multiple completions, scores them, and updates from that group of attempts. For the same model, it usually needs more room and costs more than SFT. To spend less, use a smaller model, reduce `max_completion_tokens` or `max_context_tokens`, or start with SFT. Dropping `group_size` from `8` to `4` or `2` reduces cost too, but it does **not** make a run that does not fit fit - GRPO's memory floor is effectively flat in the group. The next entry below covers what to change when a run does not fit. If you pinned `[gpu] count` (or `--gpus N`), that ceiling is the usual cause: the error names the smallest count that would fit, so raise it or drop the pin and let Flash auto-size. When nothing fits even at eight cards, the error names the knobs that actually shrink **that** algorithm. They are not the same across algorithms: * **GRPO** responds to `max_context_tokens`, `max_completion_tokens`, and `lora_rank`. Required memory is effectively flat in `prompts_per_step`, so lowering it does not help here. * **OPD** rollout concurrency is `prompts_per_step x group_size`, so lowering either shrinks the run, as do `max_completion_tokens` and `max_context_tokens`. Distillation needs no group variance, so `group_size = 1` is fine. * **SFT** responds to `max_context_tokens`, `lora_rank`, and `batch_size`. A larger base model than the task needs is the other common cause. If you recently enabled `thinking = true`, reasoning and the final answer share the same token budget. ## Training runs A successful run bills at the accepted quote; a cancellation is prorated and capped at that quote. In the preview, `Setup` is not billed, while `Train` and required finalization contribute to `Billable`. Review `flash train config.toml --cost`, reduce the run horizon or `save_at_steps` cadence if needed, and see [Charges and cancellations](/reference/cost-model#charges-and-cancellations). Warmup can take several minutes, especially for GRPO, OPD, and larger models. `flash runs log -f` shows `warming up (stage=...)` while it progresses. Check the quote's `Setup`, `Train`, and `Billable` fields rather than assuming all work before the first optimizer update is free. Once updates begin, the log streams metrics such as reward, `grad_norm`, `kl`, `entropy`, and completion length. If warmup stops advancing, keep following the log and use `flash runs cancel ` if you do not want the run to continue billing. Add funds, or lower the pre-flight estimate before submitting again - see [Lowering cost](/reference/cost-model#lowering-cost). The task is too hard for the model at its current ability: if no rollout ever scores, there's nothing for GRPO to reinforce. Try a stronger or larger base model, make the task easier to start, or double-check that your reward returns a positive reward for good answers. Usually the reward is not discriminative: it scores almost everything the same. Make the reward function separate better answers from worse ones so GRPO has a spread of scores to learn from. See [Environments](/guides/environments/overview). When `thinking = true`, the model emits a reasoning trace **before** its answer, and that trace counts against the **same `max_completion_tokens` budget** as the answer. A `max_completion_tokens` tuned for a non-thinking run is usually too small once reasoning is added: the reasoning eats the budget and the actual answer is truncated or never emitted. If your reward parses the answer (e.g. extracts a JSON object), it then sees nothing and scores \~0 across the board — even though the model is "working". Fixes: * **Raise `max_completion_tokens`** so the reasoning *and* the answer both fit (e.g. a task that needs \~200 answer tokens may need `max_completion_tokens = 2048` with thinking on), and make sure `max_context_tokens` is large enough to hold the prompt plus that budget. * Optionally set `thinking_length_penalty_coef` to nudge the model toward shorter reasoning so the answer reliably lands inside the budget. * Score the answer text by default. In thinking mode `response_text` remains string-compatible answer text and also exposes `response_text.completion`, `response_text.thinking`, and `response_text.raw` for rewards that intentionally inspect reasoning. The same trap applies to any **reasoning model you call as an LLM judge** from a reward: give the judge call enough `max_tokens` or it returns empty content and the judge silently scores 0. In Qwen3.5 thinking mode, the chat template treats prior and next assistant turns differently: it strips literal `` blocks from non-final assistant history, then pre-opens `\n` in the next generation prompt. A naive multi-turn SFT transcript that puts `...` in every assistant turn can therefore train on a tag layout that inference will never render. The symptom is doubled, missing, or misplaced thinking tags, or an adapter that behaves differently in training-style evals than it does when served. Fixes: * For message-shaped multi-turn SFT targets, keep intermediate assistant turns as the actual code, tool, or action text only. * Put `...` plus the final answer only in the final assistant target. * Do not add a second opener for the template's pre-opened `\n`. It is already part of the prompt. OPD refines a model that already has the output format, so [warm-start](/guides/training#warm-start-safely) from a finished SFT run and check that the teacher clearly beats the student on held-out examples: ```toml theme={null} [train] init_from_adapter = "" ``` There is no auxiliary EOS loss. For rollouts that reach `max_completion_tokens` without stopping: * Start from an SFT adapter with the desired stopping behavior. * Give the teacher a hard answer or reasoning budget in the system prompt. * Add `stop_sequences` when the task has reliable textual terminators. * Track `truncated_rollouts` to diagnose termination. If the cap is tight enough that most rollouts are cut off, OPD cannot align a teacher signal at all and the run fails naming the cap rather than exiting on an opaque subprocess status: ```text theme={null} flash OPD produced no aligned teacher signal after N rollout attempts; 47/64 rollouts were truncated at the configured max_completion_tokens=256, so the completion cap is likely too small ``` Raise `max_completion_tokens` when you see that. Text-only OPD supports single- and multi-turn environments but not tool-calling ones. Image-bearing OPD remains single-turn only. Statically visible image-bearing multi-turn inputs fail before allocation; images added later by environment execution are rejected by the worker guard. `kl_penalty_coef` must be greater than `0`. No. `Ctrl-C` during `flash train` detaches you; the run continues and can accrue billable work. Resume with `flash runs log -f`, or stop it with `flash runs cancel `. For machine-readable status, see [Run management](/reference/cli#run-management). Expected. Cancellation waits for the run to stop and release its GPU before confirming, which can take several minutes. The CLI waits that out; the run is marked `cancelled` when it completes. Flash retries a limited number of times, resuming from a saved checkpoint where it safely can and failing the run rather than silently restarting from scratch. Watch logs with `flash runs log -f` or poll status with `flash runs status -f`. If the same shape repeatedly fails before useful metrics, see [Run fit and resource use](#run-fit-and-resource-use) for the knobs that shrink your algorithm. A failed checkpoint upload now reports its **cause** in the run's heartbeat and log rather than leaving a silent gap, so `flash runs status ` and `flash runs log ` say why the save did not land. A later successful upload of the same checkpoint clears the reported failure. Training continues through a failed periodic upload - `save_every` saves are best-effort after bounded retries. Use `save_at_steps` when a specific checkpoint must exist; those saves are mandatory. See [Control the update horizon and checkpoints](/guides/training#control-the-update-horizon-and-checkpoints). The failure names how much was requested against what the card had, not just that an OOM happened: ```text theme={null} verl subprocess exited with status 1 after reporting torch.outofmemoryerror (tried to allocate 2.00 gib. gpu 0 has a total capacity of 179.06 gib of which 1.31 gib is free) ``` Those figures separate a request the card could never have served from one that missed narrowly, which point at different fixes. Host-RAM exhaustion is classified separately - a bigger card does not fix it. For the knobs that shrink each algorithm, see [Run fit and resource use](#run-fit-and-resource-use). ## Serving A run cancelled or preempted before finalizing has no final adapter, so a plain `flash models deploy ` cannot serve it. The error lists the run's saved checkpoint steps and the exact command to use — deploy one of them instead: ```bash theme={null} flash runs checkpoint flash models deploy /step- ``` Expected: every deploy runs a bounded smoke before the alias routes to the new revision, and a large model takes a few minutes to warm and verify. A failed smoke leaves the alias unchanged. See [Deploy & chat](/guides/deploy-and-chat#deploy). Serving is **billed per token** for requests. Prompt, completion, and cached prompt token rates are listed in [Supported models](/reference/models#serving-prices). `flash models undeploy ` disables the alias and immutable revisions. Use `openai_base_url` from `flash models deployments --json`, set `model` to the run alias or a full immutable revision, and pass your Freesolo API key. * `401`: missing or invalid key. * `402`: insufficient org balance. * `403`: the key's org does not own the adapter. * `503`: temporarily unavailable; retry with backoff. See [Use it from your own code](/guides/deploy-and-chat#use-it-from-your-own-code). ## Getting help Still stuck? Reach out through [freesolo.co/contact](https://freesolo.co/contact) with the run id (`flash runs list`), the failing command, and its `--debug` traceback.