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

# Python SDK

> starforge.report — get curves into the console from any training script.

```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})

for step, batch in enumerate(loader):
    loss = train_step(batch)
    log({"loss": loss, "reward": batch_reward}, step=step)

finish()
```

Three functions. No training framework is imported, nothing is subclassed, and the module works in
any image that has `starforge-core` installed.

## Install

Already present in every catalog image. For a custom image:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
pip install starforge-core
```

## The design constraints, because they change how you use it

<Columns cols={3}>
  <Card title="Never raises" icon="shield-check">
    Reporting is a side channel. If collection breaks, your training keeps running. You do not need
    to wrap calls in `try`.
  </Card>

  <Card title="No-op without credentials" icon="plug">
    Running the same script on your laptop does nothing and makes no network calls, so there is no
    separate "local mode" branch to maintain.
  </Card>

  <Card title="Imports anywhere" icon="package">
    Standard library only. It will not fail to import because an image is missing `requests`.
  </Card>
</Columns>

## `init()`

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
init(
    hparams: Mapping[str, Any] | None = None,
    monitor_hardware: bool = True,
    monitor_interval: float | None = None,
) -> bool
```

Starts the reporting session. Returns whether reporting is actually on — `False` on a laptop, and
that is not an error. Idempotent: calling it twice is safe, and passing `hparams` the second time
adds them.

<ParamField path="hparams" type="Mapping">
  Hyperparameters for the console's Config panel. Nested dictionaries are flattened with dots, so
  `{"policy": {"lr": 1e-6}}` becomes `policy.lr`.
</ParamField>

<ParamField path="monitor_hardware" type="bool" default="True">
  Starts a background thread that samples GPU utilisation, memory and network for the System tab.
  Set `False` if something else in your job already reports hardware.
</ParamField>

<ParamField path="monitor_interval" type="float" default="10">
  Seconds between hardware samples. `STARFORGE_MONITOR_INTERVAL` overrides the default.
</ParamField>

## `log()`

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
log(metrics: Mapping[str, Any], step: int | None = None, prefix: str = "") -> None
```

Sends one batch of scalars.

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
log({"loss": 0.42, "reward": 0.71})                    # step auto-increments
log({"loss": 0.41}, step=121)                          # explicit step
log({"loss": 0.41}, prefix="eval")                     # becomes eval/loss
log({"grad": {"norm": 1.2, "clip": 0.8}}, step=121)    # becomes grad.norm, grad.clip
```

Non-scalar values are reduced to their mean where that is meaningful and dropped where it is not — a
tensor of per-token losses becomes one number, a string becomes nothing. Omitting `step` increments
an internal counter, which is what you want in a reward function that has no notion of a global step.

<Tip>
  `log()` calls `init()` for you if you have not. Inside a reward function or an environment, a bare
  `from starforge.report import log` is enough — no setup, no plumbing an object through.
</Tip>

## `log_hparams()` and `finish()`

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
log_hparams(params: Mapping[str, Any]) -> None
finish() -> None
```

`log_hparams` adds to the Config panel after `init`. `finish` stops hardware collection and flushes
the buffer; it is idempotent and also registered with `atexit`, so a script that exits normally does
not strictly need to call it. Call it anyway — a process killed before `atexit` runs loses whatever
is still buffered.

## Hugging Face and TRL

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from starforge.report import StarForgeCallback
from trl import GRPOTrainer

trainer = GRPOTrainer(..., callbacks=[StarForgeCallback()])
```

Handles `init`, per-log-step `log`, and `finish` on the Trainer's own lifecycle. Works with any
`transformers.Trainer` and every TRL trainer built on it.

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
StarForgeCallback(monitor_hardware: bool = True, prefix: str = "")
```

It does not subclass `transformers.TrainerCallback` — Hugging Face dispatches callbacks by method
name, so duck typing is enough, and not subclassing keeps `starforge.report` importable in an image
with no `transformers`.

## Confirm it worked

Submit the job and open its Charts tab. A point appears within a few seconds of your first `log()`.

If the logs are moving and the charts stay empty, the reporting call is not running. Check inside
the container:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
echo $STARFORGE_ENABLED   # must be 1
```

If it is `1` and there are still no points, `init()` was never reached — put a `print` next to it
and resubmit.

## The rest of the SDK

`starforge.report` is the public surface. The package also exports the JobSpec contract types
(`JobSpec`, `Recipe`, `ResourceSpec`, …) and `Reporter`, the low-level client the platform's own
framework bridges use. Those are documented as
[the ingest contract](/en/api-reference/ingest) and [JobSpec](/en/reference/jobspec); reach for them
when writing a framework adapter, not when instrumenting a training script.
