> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuneplane.ai/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 tuneplane.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](https://docs.tuneplane.ai/en/api-reference/python-sdk.md) 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 |
| - | - |
| `TUNEPLANE_JOB_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 |
| `TUNEPLANE_JOB_ENDPOINT` | Base URL for the ingest routes, already including `/api/ingest` |
| `TUNEPLANE_JOB_RUN_ID` | The run these reports belong to. Every payload repeats it |
| `TUNEPLANE_JOB_TOKEN` | An [ingest token](https://docs.tuneplane.ai/en/api-reference/authentication.md) scoped to this run and valid for 30 days |

Authenticate with `X-TunePlane-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`.

> **POST /api/ingest/lifecycle — say when training actually began**
>
> ```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.

> **POST /api/ingest/metrics — the curves**
>
> ```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.

> **POST /api/ingest/hparams — the config panel**
>
> ```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.

> **POST /api/ingest/validation — sample conversations and reward histograms**
>
> ```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`.

> **POST /api/ingest/logs — stdout and stderr**
>
> ```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.

> **POST /api/ingest/artifact — register what you produced**
>
> ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> {
>   "run_id": "run-4f2a91",
>   "kind": "checkpoint",
>   "path": "s3://tuneplane/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.

> **POST /api/ingest/hardware and /environment — the System tab**
>
> `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`.

> **POST /api/ingest/benchmark — externally produced scores**
>
> For a harness that scores outside the platform and reports back. See
> [benchmarks](https://docs.tuneplane.ai/en/guides/benchmarks.md) for the workflow that surrounds it.

## Check 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 `TUNEPLANE_JOB_ENABLED` is `1` inside the container, and that your code reached `init()`.

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.