> ## Documentation Index
> Fetch the complete documentation index at: https://starforge.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Recipe packs

> Add a training method to the catalog without a platform release.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf plugin publish ./my-grpo-variant
sf methods                              # your method is now in the catalog
sf new my-exp --method nemo-rl/alice.my-grpo
```

A recipe pack declares a post-training method: which entrypoint runs, which hyperparameters are
tunable, which framework versions and images it supports, and which metrics the console should chart.
It contains no code the platform executes.

## Layout

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
my-grpo-variant/
├── plugin.yaml       # kind: recipe
├── recipe.yaml       # the declaration
├── README.md
└── template/         # what `sf new` copies into a user's repository
    ├── config.yaml
    └── run.py
```

```yaml plugin.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
schema: forge/plugin/v1
name: my-grpo
version: 1.0.0
kind: recipe
summary: GRPO with a length-normalised advantage
```

<Warning>
  Executable files are refused everywhere except `template/`. That directory is scaffolding handed to
  the user and run in *their* container; the root is parsed by the platform and must contain nothing
  it could be expected to import.
</Warning>

## recipe.yaml

```yaml recipe.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
name: my-grpo
version: "0.7.0"
framework: nemo-rl
title: GRPO with length-normalised advantage
summary: >
  Group-relative advantage divided by completion length, which stops long answers
  from dominating the batch on verbose datasets.

runtime:
  default_version: "0.7.0"
  versions:
    "0.7.0":
      requires: []
      runtime_id: "nemo-rl-0.7.0"
      source:
        kind: oci
        reference: "nvcr.io/nvidia/nemo-rl:v0.7.0@sha256:75c4821c…"
        ray_version: "2.55.1"

template: template
config_section: grpo
roles: [actor, reference, rollout]

adapter_options:
  config_mode: dot-overrides
  observability: platform
  ray_cluster_profile: nemo-rl-baseline

entrypoint:
  kind: file
  value: examples/run_grpo.py
  experiment_override: run.py

params:
  num_prompts_per_step:
    type: int
    path: grpo.num_prompts_per_step
    group: Training & validation
    doc: Prompts sampled per step
    default: 32
    min: 1
  length_norm_power:
    type: float
    path: grpo.length_norm_power
    group: Advantage
    doc: Exponent on completion length in the advantage denominator
    default: 1.0
    min: 0.0
    max: 2.0

metrics:
  primary:
    - train/reward
    - validation/accuracy
    - train/kl_penalty
    - train/loss
  aliases:
    train/entropy: [train/approx_entropy]

validation:
  sample_kind: conversation
  reward_semantics: scalar

artifacts:
  version: "forge/artifacts/v1"
  checkpoints: ["checkpoints/step_*", "step_*"]
  logs: ["logs"]
  exports: ["hf_export"]
  formats:
    checkpoint: nemo-checkpoint
    log: text
    hf_export: huggingface
```

### Parameter declarations

Each entry under `params:` is what makes `sf validate` able to reject a typo on your laptop instead
of on the cluster.

<ParamField path="type" type="int | float | str | bool | enum" required>
  Anything else is refused at load with the parameter name.
</ParamField>

<ParamField path="path" type="dot.path">
  Where the value lands in the training framework's own config. This is what lets the platform
  translate a flat `--set length_norm_power=1.2` into the framework's native override.
</ParamField>

<ParamField path="group" type="string">
  Display grouping in the console and CLI. No effect on validation.
</ParamField>

<ParamField path="default, min, max, choices, required" type="mixed">
  The validation contract. `min`/`max` are inclusive unless `exclusive_minimum` is set.
</ParamField>

<ParamField path="path_overrides" type="{version: path}">
  For when upstream moves a config key between framework versions. Declare the move here rather than
  branching on the version inside adapter code.

  ```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
  warmup_ratio:
    type: float
    path: optim.warmup_steps_ratio
    path_overrides:
      "0.9.0": optim.lr_warmup_steps_ratio
  ```
</ParamField>

### Metrics contract

`metrics.primary` is an ordered list; the first two go on the overview chart, and the diagnosis
thresholds read the same keys. `aliases` maps a canonical key to whatever names a framework version
actually emits, so a rename upstream does not blank a chart.

### Bilingual display copy

Write the manifest in English and carry translations in one `i18n:` block. The console asks for the
reader's language and falls back to the manifest text where a translation is missing.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
title: GRPO with length-normalised advantage
summary: >
  Group-relative advantage divided by completion length.
params:
  num_prompts_per_step:
    type: int
    path: grpo.num_prompts_per_step
    group: Training & validation
    doc: Prompts sampled per step
i18n:
  zh:
    title: GRPO · 长度归一化优势
    summary: >
      把组相对优势除以生成长度，避免长回答在 verbose 数据集上主导整个 batch。
    params:
      num_prompts_per_step: {group: 训练与验证, doc: 每步题目数}
```

Parameter translations live under the locale rather than on each parameter. A recipe declares dozens
of parameters, and an `i18n:` key on every one would bury the contract — path, type, range — that
a reader opens the file to find. Translating a parameter that was never declared is an error, so a
rename cannot leave a translation pointing at nothing.

Only display copy is translatable. Names, paths and metric keys are the contract and read the same
in every language.

## Naming

A published method's id is always `<framework>/<owner>.<name>`:

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
nemo-rl/alice.my-grpo
```

That namespace segment makes a collision with a built-in method structurally impossible — you cannot
publish something that shadows `nemo-rl/grpo`.

## Images

<Warning>
  A recipe declares which container image the job runs. That is the one genuinely dangerous thing a
  declarative pack can express: plain YAML could otherwise make the cluster pull any image.

  Images must pass the deployment's registry allowlist (`FORGE_ALLOWED_IMAGE_REGISTRIES`). Configure
  it before opening recipe publishing to users.
</Warning>

## Confirm it worked

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf recipe sync                          # pull the catalog (runs automatically before submit)
sf methods                              # your method appears with a source badge
sf methods nemo-rl/alice.my-grpo        # its tunable parameters
sf new probe --method nemo-rl/alice.my-grpo
sf validate probe
```

`sf validate` exercises your parameter declarations without touching a cluster. If a bad value
passes there, the declaration is missing a range.

## Versioning

`version` in `recipe.yaml` tracks the upstream framework release. The pack's content identity is a
separate bundle digest over the manifest plus the template, so changing a default or fixing a
template updates the digest without pretending the framework changed.

An experiment pins the digest in `recipe.lock.json`. When you publish a new version, existing
experiments keep working until their owner runs `sf recipe upgrade`.
