> ## 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.

# Reporting from training code

> The contract a running job speaks to put curves, samples, logs and artifacts in the console.

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from starforge.report import init, log, finish

init(hparams={"lr": 1e-6, "kl_coef": 0.05})
log({"loss": 0.42, "reward": 0.71}, step=120)
finish()
```

That is the whole thing for most people — [the Python SDK](/en/api-reference/python-sdk) speaks this
contract for you. Read on only if you are writing an adapter for a framework the catalog does not
cover, or a client in another language.

## What the platform injects

Four environment variables arrive in every training container. Your code reads them; it never
receives account credentials, cluster addresses, or object-store keys.

| Variable             | Meaning                                                                                                                                                                          |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `STARFORGE_ENABLED`  | `1` when reporting is wired up, `0` when the job is deliberately offline. Any other value, including unset, is a configuration error and should raise rather than silently no-op |
| `STARFORGE_ENDPOINT` | Base URL for the ingest routes, already including `/api/ingest`                                                                                                                  |
| `STARFORGE_RUN_ID`   | The run these reports belong to. Every payload repeats it                                                                                                                        |
| `STARFORGE_TOKEN`    | An [ingest token](/en/api-reference/authentication) scoped to this run and valid for 30 days                                                                                     |

Authenticate with `X-StarForge-Token: <token>`. The routes also accept
`Authorization: Bearer <token>`, which is what a client that already has a bearer helper will do
naturally.

## The endpoints

All are `POST`, all take JSON, all include `run_id`.

<AccordionGroup>
  <Accordion title="POST /api/ingest/lifecycle — say when training actually began" icon="play">
    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    { "run_id": "run-4f2a91", "event": "running", "ts": "2026-08-31T09:14:22+00:00" }
    ```

    Events are `starting`, `running`, `succeeded`, `failed`.

    Report `running` immediately before you hand control to the training entrypoint. The executor's
    own RUNNING is much earlier — it fires before the virtualenv is built and the weights are pulled,
    which can be several minutes — so using it as the billing start systematically overcharges.
  </Accordion>

  <Accordion title="POST /api/ingest/metrics — the curves" icon="chart-line">
    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "run_id": "run-4f2a91",
      "points": [
        { "key": "train/loss", "step": 120, "value": 0.42, "ts": "2026-08-31T09:14:22+00:00" },
        { "key": "train/reward", "step": 120, "value": 0.71, "ts": "2026-08-31T09:14:22+00:00" }
      ]
    }
    ```

    Returns `{"ok": true, "inserted": 2}`. Points are already flattened: one scalar per key, per step.
    Nested dictionaries and non-scalar values are the caller's problem to reduce first.
  </Accordion>

  <Accordion title="POST /api/ingest/hparams — the config panel" icon="sliders">
    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    { "run_id": "run-4f2a91", "params": { "policy.optimizer.kwargs.lr": 1e-6, "grpo.kl_coef": 0.05 } }
    ```

    Upserts, so calling it again with more keys adds them. Flatten nested config with dots.
  </Accordion>

  <Accordion title="POST /api/ingest/validation — sample conversations and reward histograms" icon="list-checks">
    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "run_id": "run-4f2a91",
      "step": 120,
      "samples": [
        {
          "user": "A train leaves Chicago at 3pm...",
          "assistant": "Let me work through this step by step...",
          "reward": 0.83,
          "extra": { "reference": "4 hours" }
        }
      ],
      "avg_reward": 0.71,
      "accuracy": 0.64,
      "chunk_index": 0,
      "total_chunks": 1
    }
    ```

    `user`, `assistant`, `env`, and `reward` are the skeleton the console renders. Anything
    algorithm-specific — DPO's rejected completion, SFT's reference answer — goes in each sample's
    `extra` and is displayed without the platform needing to know what it is.

    Large validation rounds split across chunks: set `total_chunks` and send each with its own
    `chunk_index`.
  </Accordion>

  <Accordion title="POST /api/ingest/logs — stdout and stderr" icon="scroll-text">
    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    { "run_id": "run-4f2a91", "chunks": ["step 120 | loss 0.42\n"], "eof": false }
    ```

    The one channel where a caller must swallow its own errors. Every other ingest route fails loudly
    because losing a metric or an artifact is a data incident; losing a few log lines is not, and a
    momentary console hiccup must never turn a successful training run into a failed one.

    Do not send `eof` yourself. The platform sets it when the job reaches a terminal state, because a
    container killed with SIGKILL never gets the chance.
  </Accordion>

  <Accordion title="POST /api/ingest/artifact — register what you produced" icon="package">
    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "run_id": "run-4f2a91",
      "kind": "checkpoint",
      "path": "s3://starforge/runs/run-4f2a91/checkpoints/step-500",
      "format": "distcp",
      "step": 500,
      "size_bytes": 14203847362
    }
    ```

    Kinds are `checkpoint`, `hf_export`, `eval_report`, `merged_model`. `format` is required and must
    not be empty.

    On a container executor, `path` **must** be an object-store URI. A local path stops existing the
    moment the container does, and the platform will not guess where it went. `GET /api/ingest/artifact/upload-url`
    hands you a signed URL to PUT to first.
  </Accordion>

  <Accordion title="POST /api/ingest/hardware and /environment — the System tab" icon="server">
    `hardware` takes sampled points (GPU utilisation, memory, network). `environment` takes the static
    picture once: package versions, CUDA version, GPU models. `environment/nodes` reports per-node
    hardware for a multi-node run.

    The SDK collects and sends all three for you unless you pass `monitor_hardware=False`.
  </Accordion>

  <Accordion title="POST /api/ingest/benchmark — externally produced scores" icon="gauge">
    For a harness that scores outside the platform and reports back. See
    [benchmarks](/en/guides/benchmarks) for the workflow that surrounds it.
  </Accordion>
</AccordionGroup>

## Confirm it worked

Open the job in the console. The Charts tab shows a point within a few seconds of your first
`metrics` call. If logs are streaming and charts stay empty, the reporting call is not happening —
check that `STARFORGE_ENABLED` is `1` inside the container, and that your code reached `init()`.

<Accordion title="Why reporting lives in the SDK rather than in the uploaded working directory">
  Lifecycle marks and artifact registration are how the platform knows a job really started and what
  it produced. If that code shipped inside the user's own `common/` directory, deleting or breaking
  that directory would blind the platform — and the person doing it would have no way to know. It is
  also why the module depends on nothing but the standard library: it has to import cleanly inside
  any training image, and every dependency is one more way for that to fail.
</Accordion>
