---
slug: cookbook/openai-api
title: Run H3 with a server and playground
---
<a id="run-h3-with-a-server-and-playground"></a>

Start FastH3 on CUDA, one DGX Spark, or Apple Silicon MLX, then iterate on prompts in a browser,
with cURL, or from your app. The server and clients can run on the same machine.

The server supports the OpenAI-compatible video-job API. The Python and
JavaScript client examples use that interface, but requests go to FastVideo.
You do not need an OpenAI account or cloud key.

The [H3 recipe selector](/FastVideo/cookbook/minimax-h3) provides the same workflow with runtime
selection. This guide covers FastH3 V1 (four forwards) and FastH3 V2
(eight forwards). Start a server, then iterate in the playground or with the
OpenAI Python client. Other H3 recipes keep their direct Python commands.

CUDA requests reuse one loaded `VideoGenerator`. The Python SDK can do the same
when you reuse the generator across `generate()` calls. MLX keeps one
`MiniMaxH3MLXPipeline` and a prompt-embedding cache, but still loads and releases
components between phases to fit unified memory. MLX serving does not keep all
weights resident or remove phase-loading time. Use a server when you want
separate clients to share the process without restarting scripts.

## Install and start the server {#install-and-start-the-server}

### CUDA {#cuda}

Use a FastVideo clone and an activated Python environment. Complete the
[CUDA installation requirements](/FastVideo/getting_started/installation/gpu)
before running these commands:

```bash
UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"
fastvideo serve --config examples/serving/openai_fasth3.yaml --server.host 127.0.0.1
```

The configuration loads `FastVideo/FastVideo-Minimax-FastH3-Preview-v0.2` and
advertises it as `fasth3`. It configures four CUDA GPUs but does not record
a GPU model or VRAM requirement. This is a source-backed server profile, not
the measured GB200 Python performance profile. Compilation is disabled.

For FastH3 V2, use the same install and the 8-step config (nine sigma
points, eight DiT forwards, VSA sparsity 0.8):

```bash
UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"
fastvideo serve --config examples/serving/openai_fasth3_8step.yaml --server.host 127.0.0.1
```

Keep the server running. In another terminal, check readiness:

```bash
curl --fail-with-body http://127.0.0.1:8000/health
```

After model loading completes, the response is `{"status":"ok"}`.

### NVIDIA DGX Spark {#nvidia-dgx-spark}

Complete the [DGX Spark installation](/FastVideo/getting_started/installation/spark)
before running these commands. GB10 has no FA4 / sm_100a VSA kernel:

```bash
UV_TORCH_BACKEND=cu130 uv pip install -e .
FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 \
  fastvideo serve --config examples/serving/openai_fasth3_spark.yaml --server.host 127.0.0.1
```

The configuration loads `FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree`
on one GB10 and advertises it as `fasth3`. Lazy module load still reloads
Qwen3-VL and the DiT between phases of each request. Legal `num_frames` values
are `17n+5`, capped at 362 (15.08 s); a 345-frame request on one Spark can OOM.
There is no cookbook server for two Sparks; use the generate YAML after
[pairing two Sparks](/FastVideo/getting_started/installation/spark_pair).

Keep the server running. In another terminal, check readiness:

```bash
curl --fail-with-body http://127.0.0.1:8000/health
```

After model loading completes, the response is `{"status":"ok"}`.

### Apple Silicon MLX {#apple-silicon-mlx}

Complete the [MLX install](/FastVideo/getting_started/installation/mlx),
including `ffmpeg`. From your FastVideo clone, install the MLX extra:

```bash
uv pip install -e ".[mlx]"
```

Download and convert the weights once. Skip this step if you already have the
snapshot and converted DiT:

```bash
hf download FastVideo/FastVideo-Minimax-FastH3-Preview-v0.2 --local-dir ./FastH3-Preview-v0.2
python scripts/checkpoint_conversion/convert_minimax_h3_mlx.py --model-root ./FastH3-Preview-v0.2/transformer --out ./FastH3-MLX --formats "int6"
```

Edit `generator.model_root` and `generator.mlx_checkpoint` in
`examples/serving/mlx_fasth3.yaml` if your weights are elsewhere. Paths are
relative to the directory where you launch the server. Start it with:

```bash
python -m fastvideo.entrypoints.openai.mlx_server --config examples/serving/mlx_fasth3.yaml
```

This uses the native MLX pipeline, not PyTorch MPS. The default output is
832 × 480, 124 frames at 24 fps, with four DiT forwards and the full H3 VAE.
The HTTP field `num_inference_steps=5` describes five sigma points; the adapter
passes `num_steps=4` to MLX. Temporal/spatial fast modes, VSA, reference inputs,
LoRA selection, and alternate decoders are not exposed by this server adapter.
Unsupported request options return HTTP 400 before a job starts.

The server binds to `127.0.0.1:8000` and advertises `fasth3`, so the playground
and clients below work unchanged. Do not run CUDA, Spark, and MLX servers on the
same port. MLX readiness means the pipeline is initialized; components load during
generation. The first request can take longer than a repeated cached prompt.

The MLX server has no recorded device or unified-memory requirement. The
direct Python recipe's M4 Max measurements are not a server benchmark or a
minimum-memory claim.

For FastH3 V2, convert that checkpoint's transformer with `--include-vsa`
and start the 8-step config. Do not overwrite a V1 export. The
[MiniMax H3 cookbook](/FastVideo/cookbook/minimax-h3) has the same download and convert
commands as the Python recipe.

```bash
hf download FastVideo/FastVideo-FastH3-8-Step-V2 --local-dir ./FastH3-8-Step-V2
python scripts/checkpoint_conversion/convert_minimax_h3_mlx.py --model-root ./FastH3-8-Step-V2/transformer --out ./FastH3-8-Step-V2-MLX --formats "int8" --include-vsa
python -m fastvideo.entrypoints.openai.mlx_server --config examples/serving/mlx_fasth3_8step.yaml
```

That adapter passes `num_steps=8` when the HTTP field is `num_inference_steps=9`,
and it enables the trained VSA recipe (sparsity 0.8, 64-token tiles). Reuse the
preview VAE, audio VAE, text encoder, and tokenizer if those directories already
exist; edit the YAML paths if your files live elsewhere.

## Open the playground {#open-the-playground}

Open [the local H3 playground](http://127.0.0.1:8000/playground/) after startup.
Write a prompt, optionally set a seed, then select **Generate video**. The page
checks the job status and shows the completed video with a download link. Edit
the prompt and generate again without restarting the server.

The playground sends requests to `/v1/videos` on the same server that serves
the page. **Use this prompt with cURL** shows the equivalent submission. Recent
jobs include requests from other clients, so a script and the playground can
use the same model process. Opening the page or copying a command does not
start generation.

The page URL includes the active job ID. Reloading that URL resumes status
checks without resubmitting the prompt. A failed connection or a 30-minute
polling timeout does not cancel execution. Select **Check status** to reconnect.
If submission itself is interrupted, check Recent jobs before submitting again.

This first playground supports H3 text-to-video/audio. Reference-media inputs
remain available through the API, not through the playground. It does not start
or manage a GPU server for you.

## Generate with cURL or an SDK {#generate-with-curl-or-an-sdk}

These examples use the server's resolution, frame count, and sampling defaults.
Do not copy Sora-specific durations or resolutions onto H3. V1 configs use
124 frames, 24 fps, and the five-point distilled sigma schedule with four DiT
forwards. V2 configs use nine sigma points and eight DiT forwards. CUDA
and one Spark use 1344 × 768; MLX uses 832 × 480. The OpenAI Python client is
the same for every FastH3 server that advertises `fasth3`.

Each client submits a job, checks for completion or failure, and downloads an
MP4 named after the job ID. Polling stops after 30 minutes; a timeout does not
cancel GPU execution. Keep the printed job ID to retrieve its status later.
Transport retries are disabled to avoid accidental duplicate submissions.

### OpenAI Python {#openai-python}

Install the tested client, then run the checked-in example:

```bash
python -m pip install openai==3.6.0
python examples/serving/clients/video.py
```

```python
# SPDX-License-Identifier: Apache-2.0
"""Generate with a local FastVideo server through the OpenAI Python client.

Install the tested client with: python -m pip install openai==3.6.0
Start the server first. No OpenAI account or cloud API key is needed.
"""

import os
import time
from pathlib import Path

from openai import OpenAI


def main() -> None:
    with OpenAI(
            base_url=os.environ.get("FASTVIDEO_BASE_URL", "http://127.0.0.1:8000/v1"),
            api_key=os.environ.get("FASTVIDEO_API_KEY", "local"),
            timeout=60.0,
            max_retries=0,
    ) as client:
        video = client.videos.create(
            model=os.environ.get("FASTVIDEO_MODEL", "fasth3"),
            prompt="A fox runs through fresh snow.",
        )
        print(f"Submitted {video.id}")

        # This deadline limits polling, not GPU execution. Increase it for
        # longer jobs. On timeout, use the printed ID to retrieve the job.
        deadline = time.monotonic() + 1800
        while video.status in {"queued", "in_progress"}:
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                raise TimeoutError(f"Polling timed out; job {video.id} may still be running")
            time.sleep(min(2, remaining))
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                raise TimeoutError(f"Polling timed out; job {video.id} may still be running")
            video = client.videos.retrieve(video.id, timeout=min(60, remaining))

        if video.status != "completed":
            message = video.error.message if video.error else video.status
            raise RuntimeError(f"Video {video.id} failed: {message}")

        output = Path(f"{video.id}.mp4")
        content = client.videos.download_content(video.id)
        content.write_to_file(output)
        print(f"Saved {output}")


if __name__ == "__main__":
    main()

```

### OpenAI JavaScript {#openai-javascript}

Use Node.js 22 or later on your computer or in your webapp's backend. Do not put a
private server key in browser code.

```bash
npm ci --prefix examples/serving/clients
node examples/serving/clients/video.mjs
```

```javascript
// Run in Node.js 22+ or your webapp's backend, not in a browser with a private key.
import OpenAI from "openai";
import { writeFile } from "node:fs/promises";

const client = new OpenAI({
  baseURL: process.env.FASTVIDEO_BASE_URL || "http://127.0.0.1:8000/v1",
  apiKey: process.env.FASTVIDEO_API_KEY || "local",
  timeout: 60_000,
  maxRetries: 0,
});

let video = await client.videos.create({
  model: process.env.FASTVIDEO_MODEL || "fasth3",
  prompt: "A fox runs through fresh snow.",
});
console.log(`Submitted ${video.id}`);

// The deadline limits polling, not GPU execution. Keep the ID to resume later.
const deadline = performance.now() + 1_800_000;
while (video.status === "queued" || video.status === "in_progress") {
  let remaining = deadline - performance.now();
  if (remaining <= 0) throw new Error(`Polling timed out; job ${video.id} may still be running`);
  await new Promise((resolve) => setTimeout(resolve, Math.min(2000, remaining)));
  remaining = deadline - performance.now();
  if (remaining <= 0) throw new Error(`Polling timed out; job ${video.id} may still be running`);
  video = await client.videos.retrieve(video.id, { timeout: Math.min(60_000, remaining) });
}

if (video.status !== "completed") {
  throw new Error(`Video ${video.id} failed: ${video.error?.message || video.status}`);
}

const content = await client.videos.downloadContent(video.id);
const output = `${video.id}.mp4`;
await writeFile(output, Buffer.from(await content.arrayBuffer()));
console.log(`Saved ${output}`);

```

### cURL {#curl}

Install `curl` and `jq`, then run:

```bash
bash examples/serving/clients/video.sh
```

```bash
#!/usr/bin/env bash
# Requires curl and jq. Run after starting the FastVideo server.
set -euo pipefail

base_url="${FASTVIDEO_BASE_URL:-http://127.0.0.1:8000/v1}"
api_key="${FASTVIDEO_API_KEY:-local}"
model="${FASTVIDEO_MODEL:-fasth3}"
payload=$(jq -n --arg model "$model" \
  '{model: $model, prompt: "A fox runs through fresh snow."}')

job=$(curl --fail-with-body --silent --show-error --max-time 60 \
  "$base_url/videos" \
  -H "Authorization: Bearer $api_key" \
  -H 'Content-Type: application/json' \
  -d "$payload")
job_id=$(jq -er '.id' <<< "$job")
printf 'Submitted %s\n' "$job_id"
deadline=$((SECONDS + 1800))

while true; do
  status=$(jq -er '.status' <<< "$job")
  case "$status" in
    completed) break ;;
    failed) jq -r '.error.message' <<< "$job" >&2; exit 1 ;;
    queued|in_progress) ;;
    *) printf 'Unexpected job status: %s\n' "$status" >&2; exit 1 ;;
  esac
  remaining=$((deadline - SECONDS))
  if (( remaining <= 0 )); then
    printf 'Polling timed out; job %s may still be running\n' "$job_id" >&2
    exit 1
  fi
  if (( remaining > 2 )); then sleep 2; else sleep "$remaining"; fi
  remaining=$((deadline - SECONDS))
  if (( remaining <= 0 )); then continue; fi
  if (( remaining > 60 )); then remaining=60; fi
  job=$(curl --fail-with-body --silent --show-error --max-time "$remaining" \
    -H "Authorization: Bearer $api_key" "$base_url/videos/$job_id")
done

curl --fail-with-body --silent --show-error --max-time 300 \
  -H "Authorization: Bearer $api_key" \
  "$base_url/videos/$job_id/content" --output "$job_id.mp4"
printf 'Saved %s.mp4\n' "$job_id"

```

## Connect your app {#connect-your-app}

Set `FASTVIDEO_BASE_URL` to the FastVideo endpoint, including `/v1`, and
`FASTVIDEO_MODEL` to its advertised model alias. The examples default to
`http://127.0.0.1:8000/v1` and `fasth3`.

For a remote GPU machine, keep the server bound to loopback and forward the
port. Replace `user@gpu-host` with your SSH destination:

```bash
ssh -N -L 8000:127.0.0.1:8000 user@gpu-host
```

Then open `http://127.0.0.1:8000/playground/` on your computer. The same forwarded
address works for the cURL and SDK clients. If local port 8000 is occupied, use
`-L 8001:127.0.0.1:8000`, open port 8001, and set `FASTVIDEO_BASE_URL` to
`http://127.0.0.1:8001/v1` for the example clients.

Your webapp backend can submit a job and return its ID to the browser. Poll
from the backend, then proxy the completed download or store it in your own
artifact store. Do not hold a browser request open for the entire generation.

The client key `local` is a placeholder required by the SDK, not server
authentication. FastVideo's HTTP server has no built-in API-key check. Before
public deployment, put it behind an authenticated TLS proxy with restricted
origins, request limits, and access controls. If your proxy uses bearer tokens,
set `FASTVIDEO_API_KEY` on your backend. Never reuse an OpenAI cloud key here.

## Compatibility and limits {#compatibility-and-limits}

The client examples target the OpenAI video-job API, not Chat Completions.
The compatibility tests cover real HTTP requests with the pinned SDKs and a
fake generator. They establish client and protocol behavior, not GPU generation
quality, latency, or memory use. The server configuration still needs a recorded
H3 hardware run before it can be marked Verified in the cookbook.

| Operation | Endpoint |
| --- | --- |
| List served models | `GET /v1/models` |
| Create a video job | `POST /v1/videos` |
| Retrieve status, including a failed job | `GET /v1/videos/{id}` |
| List jobs | `GET /v1/videos` |
| Download the MP4 | `GET /v1/videos/{id}/content` |
| Delete a job and its artifact | `DELETE /v1/videos/{id}` |

Failed jobs return HTTP 200 with `status: "failed"` and an `error` object so
SDK polling can stop. Invalid requests return HTTP 400; missing jobs return
HTTP 404. The download endpoint supports only the `video` variant, not
thumbnails or spritesheets. Remix, extensions, characters, and an OpenAI Files
store are not implemented. `/v1/videos/sync` is a FastVideo extension and is
not used by these client examples.

OpenAI marks its hosted Sora API as deprecated. FastVideo runs its own models;
the [OpenAI video API reference](https://developers.openai.com/api/reference/python/resources/videos/methods/create)
describes the client interface, not FastVideo model availability. The examples
pin tested SDK versions because future SDKs may change or remove video helpers.

The server serializes generation through one loaded pipeline. Job metadata is
in memory and is lost on restart. Async artifacts remain on disk until deleted
through the API; establish a retention policy for production use. Deleting an
in-progress job does not interrupt an already running CUDA call.

For model-specific request fields and reference media, see the
[HTTP contract](/FastVideo/design/server_contracts/openai).

