A self-hosted, MiniMax H3-compatible video generation API for the open-weight MiniMax H3 models, served through SGLang.
This is an independent project. It is not affiliated with or endorsed by MiniMax; "MiniMax" and "MiniMax H3" are used only to describe API and model compatibility.
This repository provides two independently deployable processes:
h3-service: a complete MiniMax H3-compatible API, PostgreSQL task queue, SGLang worker, callback delivery, mounted-disk asset storage and dashboard.h3-router: an optional stateless request router. It has no database, mount, cache or file-writing code. A singleh3-serviceworks without it.
The current release implements text-to-video and first/last-frame image-to-video through an FL2VA SGLang deployment, plus reference image/video/audio generation through a Ref2VA SGLang deployment.
client ───────────────────────────────► h3-service (FL2VA)
│ ├─ PostgreSQL queue/leases
│ ├─ mounted disk
│ └─ SGLang FL2VA
│
└─ optional h3-router ─┬─ T2V/I2V ──► h3-service (FL2VA)
└─ Ref ───────► h3-service (Ref2VA) ─► SGLang Ref2VA
The router sends T2V/I2V creation traffic to H3_FL2VA_SERVICE_URLS and Ref
creation traffic to H3_REF2VA_SERVICE_URLS. Query, delete, download and
dashboard traffic use the combined FL2VA and Ref2VA pool because all H3 service
instances share the same PostgreSQL database and mounted asset namespace.
Router round-robin state exists only in memory and is safe to lose. When FL2VA
and Ref2VA services run on different machines, they must share PostgreSQL and
the mounted asset namespace so any H3 service can answer task operations.
| Component | Image |
|---|---|
| H3 Server | 4pdosc/minimax-h3-server |
| H3 Router | 4pdosc/minimax-h3-router |
| SGLang (FL2VA) | lmsysorg/sglang:v0.5.19 |
| SGLang (Ref2VA) | lmsysorg/sglang:v0.5.19 |
Ref2VA requests that mix reference media with first/last frames need SGLang v0.5.19 or newer.
The Helm chart lives in modelsphere/helm-charts
as minimax-h3:
helm repo add modelsphere https://modelsphere.github.io/helm-charts
helm install h3 modelsphere/minimax-h3 -n minimax-h3 --create-namespaceSee the chart README for values, storage and upgrade notes.
Python 3.11+ is required. For local development:
./postgresql_demo/build.sh
./postgresql_demo/docker.sh
python3 -m venv .venv
.venv/bin/pip install -e '.[test]'
export H3_DATABASE_URL=postgresql://h3:h3@127.0.0.1:5432/minimaxh3
export H3_SGLANG_URL=http://127.0.0.1:30010
.venv/bin/h3-servicepostgresql_demo/ is an optional single-node PostgreSQL 16 deployment. It is
intended only for a simple manual installation and does not include HA features.
Open http://127.0.0.1:8000/dashboard for the dashboard.
Build the images on the build machine:
./h3_service/build.sh
./h3_router/build.sh
./postgresql_demo/build.shbuild.sh tags local images (minimax-h3-server:dev, minimax-h3-router:dev);
set H3_SERVER_IMAGE / H3_ROUTER_IMAGE to tag them for your registry. Builds use
PyPI by default; pass --build-arg PIP_INDEX_URL=<mirror> to use a mirror.
For a single-host Docker deployment, run the following launchers. They create or
reuse a shared bridge network (minimax-h3) and connect through container names,
so the defaults work without treating a container's loopback as the host. Set
H3_DOCKER_NETWORK consistently to use another network. The launchers default
to the published images; override them with environment
variables (H3_DATABASE_URL, H3_SGLANG_URL, H3_DATA_DIR,
H3_FL2VA_MODEL_DIR, H3_REF2VA_MODEL_DIR, H3_FL2VA_SERVICE_URLS,
H3_REF2VA_SERVICE_URLS, H3_SERVER_IMAGE, H3_ROUTER_IMAGE, H3_SGLANG_IMAGE):
./postgresql_demo/docker.sh
until docker exec minimax-h3-postgres pg_isready -U h3 -d minimaxh3; do sleep 1; done
./h3_service/sglang.sh
./h3_service/docker.sh
./h3_router/docker.shWhen services run on different machines, set H3_DATABASE_URL, H3_SGLANG_URL
and the Router's backend URLs to addresses reachable from their containers.
Docker network names and container DNS are local to one Docker host. Use a
distinct reachable SGLang URL per backend so another API replica can poll it
when taking over a task.
The API and SGLang containers must mount the same host directory at the paths
specified by H3_STORAGE_ROOT and H3_SGLANG_MEDIA_ROOT. docker.sh starts the
API container with this layout. h3_service/sglang.sh starts FL2VA with 4 GPUs,
TP 2 and Ulysses 2; h3_service/sglang_ref2va.sh starts Ref2VA with 4 GPUs,
TP 4 and Ulysses 1. The same H3 Server image and docker.sh are used for both
capabilities: H3_SERVICE_CAPABILITY=ref2va selects the Ref2VA SGLang container
on the same host; override H3_SGLANG_URL for a remote backend. The launchers use
fixed container names and published ports, so run one capability per host with
these scripts, or use the Helm chart for both pools. Download URLs are built from the
current request host and forwarded headers, so no public URL setting is needed.
No FTP service or upload is used.
Ref2VA now defaults to the LightX2V 8-step 768p Turbo LoRA. Download the exact file on every GPU node that can serve Ref2VA before starting the offline container:
hf download lightx2v/Minimax-h3-Turbo \
minimax_h3_ref2v_turbo_8step_v1.0_768p_bf16.safetensors \
--local-dir /srv/models/Minimax-h3-Turbo
./h3_service/sglang_ref2va.sh
H3_SERVICE_CAPABILITY=ref2va ./h3_service/docker.shBoth launchers source h3_service/ref2va_profile.sh. The shared switch
H3_REF2VA_TURBO_ENABLED defaults to true and controls the LoRA arguments,
mount and API sampling settings together. Use the same environment when starting
both containers. The SGLang script mounts that directory read-only at /lora, uses scale 1.0,
alpha 8, and --lora-merge-mode auto, and checks that the file exists before
replacing the container. Override the host directory with H3_REF2VA_LORA_DIR.
The original Ref2VA base checkpoint remains mounted at /model.
Request-based warmup uses --warmup-steps 2: H3 requires both endpoints of the
sigma interval, so SGLang v0.5.19's default one-point warmup is invalid.
The H3 Server sends num_inference_steps=9, flow_shift=6.0,
audio_flow_shift=3.0, and quality="lossless" for Ref2VA. In SGLang v0.5.19,
nine sigma points mean eight denoiser evaluations. lossless disables additional
sampling approximations; the output still reflects the distilled LoRA.
The three settings are configurable through H3_REF2VA_NUM_INFERENCE_STEPS,
H3_REF2VA_FLOW_SHIFT, and H3_REF2VA_AUDIO_FLOW_SHIFT; FL2VA keeps its existing
50-step settings. When starting the Server directly, leave all three unset (or
blank) for base mode, or set all three together. Steps accept integer-valued
numeric strings such as 9.0; invalid Ref2VA values do not affect FL2VA startup.
Keep the LoRA file and sampling settings consistent across all
Ref2VA replicas and recheck the step convention when upgrading SGLang.
To return to base Ref2VA, set the shared switch to false for both launchers:
export H3_REF2VA_TURBO_ENABLED=false
./h3_service/sglang_ref2va.sh
H3_SERVICE_CAPABILITY=ref2va ./h3_service/docker.shThis removes the LoRA arguments and mount and omits num_inference_steps,
flow_shift, audio_flow_shift and quality from Ref2VA requests, restoring
the original backend defaults. Warmup remains at two steps in both modes.
Pause new Ref2VA submissions and drain queued and running tasks before changing
all replicas together. Request bodies and the public model name MiniMax-H3
remain the same.
H3_SERVER_IMAGE overrides the image for both the build and API launch scripts.
The API launcher checks the image's Ref2VA config protocol before removing the
current container, in both Turbo and base mode; Server images built before this
protocol existed are rejected. For Kubernetes, see the chart README in
modelsphere/helm-charts.
The router uses a separate minimal image without PostgreSQL/Pillow dependencies.
Its container is read-only and receives no database or storage settings. For
multiple backends, set comma-separated URLs in H3_FL2VA_SERVICE_URLS and
H3_REF2VA_SERVICE_URLS when running h3_router/docker.sh.
Environment variables and the full API are described in api.md (in Chinese).
T2V:
curl -X POST http://127.0.0.1:8000/v2/video_generation \
-H 'Content-Type: application/json' \
-d '{
"model": "MiniMax-H3",
"content": [{"type": "text", "text": "A fox walks through snow"}],
"resolution": "768P",
"duration": 5,
"ratio": "16:9"
}'I2V accepts image_url content with role: first_frame, last_frame, or both.
When the role is omitted it defaults to first_frame. Images may be HTTP(S) or
data URLs. Only 768P and durations from 4 to 15 seconds are accepted.
{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "The camera slowly pushes forward"},
{"type": "image_url", "role": "first_frame", "image_url": {"url": "https://example.com/first.jpg"}},
{"type": "image_url", "role": "last_frame", "image_url": {"url": "https://example.com/last.jpg"}}
],
"resolution": "768P",
"duration": 5,
"ratio": "adaptive"
}Ref2VA accepts reference images, videos and audio using the roles
reference_image, reference_video and reference_audio. A reference audio
must be accompanied by at least one reference image or video. The Router sends
these requests to the Ref2VA H3 Server pool.
Ref2VA also accepts first_frame and/or last_frame images alongside reference
media (one image per frame role). These requests remain in the Ref2VA pool and
keep the requested ratio. This combination requires SGLang v0.5.19 or newer.
Reference-media limits exclude the optional first/last frames;
keyframes do not replace the visual reference required by reference_audio.
{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "Keep the subject from <Picture 1> and transition between the supplied frames"},
{"type": "image_url", "role": "reference_image", "image_url": {"url": "https://example.com/reference.jpg"}},
{"type": "image_url", "role": "first_frame", "image_url": {"url": "https://example.com/first.jpg"}},
{"type": "image_url", "role": "last_frame", "image_url": {"url": "https://example.com/last.jpg"}}
],
"resolution": "768P",
"duration": 5,
"ratio": "16:9"
}Task operations:
GET /v2/query/video_generation/{task_id}
GET /v2/query/video_generation
DELETE /v2/video_generation/{task_id}
GET /v2/video_generation/{task_id}/content
The content endpoint supports HTTP byte ranges. This version deliberately has no Bearer authentication; place the service behind your own authenticated gateway before exposing it publicly.
If callback_url is supplied, task creation first sends {"challenge":"..."}.
The endpoint must return HTTP 200 with the same JSON field. State notifications
then contain {"task": ...} for queued/running/terminal transitions and are
retried up to H3_CALLBACK_ATTEMPTS times.
By default, image and callback URLs that resolve to loopback, private, link-local,
multicast or reserved addresses are rejected, credentials in URLs are rejected,
redirects are revalidated, downloads are bounded, and callback redirects are not
followed. This reduces SSRF risk. Set H3_ALLOW_PRIVATE_URLS=true only in a
trusted network where private image/callback endpoints are required.
The task store claims work with PostgreSQL FOR UPDATE SKIP LOCKED and renewable
leases. In addition, it:
- keeps assets as
h3_task_assets.relative_pathon a shared mount; - splits task, one-to-many assets and callback deliveries into separate tables;
- stores official request content, mode/capability and SGLang backend job identity;
- uses lowercase official task states and soft deletion;
- adds callback leases/idempotency and advisory-locked cleanup;
- separates the 7-day query window from 30-day assets/dashboard metadata.
h3_video_tasks, h3_task_assets and h3_callback_deliveries are created
idempotently at startup. Official query APIs stop returning
a task after 7 days. Inputs, thumbnails and outputs are removed from the mounted
disk after 30 days; terminal metadata is then purged after the dashboard window.
The query and asset windows are configurable; dashboard metadata retention is
fixed at 30 days.
GET /healthz liveness
GET /readyz database/storage initialization completed
GET /metrics Prometheus gauges by task status (24h window, legacy)
GET /metrics/queue Prometheus live queue/backend gauges (see Metrics)
GET /dashboard operator dashboard
Workers persist the SGLang job id before polling. PostgreSQL leases and heartbeats allow another service instance to resume polling after a crash without submitting a second job. A submission timeout is treated as ambiguous and fails the task to avoid accidental duplicate GPU work.
On the target server, a useful first-run check is:
curl -f http://127.0.0.1:8000/readyz
curl -f http://127.0.0.1:8000/healthz
psql "$H3_DATABASE_URL" -c '\dt h3_*'Two Prometheus text endpoints, split on purpose:
GET /metrics— legacy, kept byte-stable for existing dashboards/alerts. A single gaugeh3_tasks{status=...}counting tasks created in the last 24h and currently in that state (status:queued,running,succeeded,failed,cancelled). Do not rename or relabel it.GET /metrics/queue— the live queue/backend view. This is what the Helm chart'sServiceMonitorscrapes (serviceMonitor.path, default/metrics/queue,interval30s /scrapeTimeout10s). Newh3_queue_*names, safe to extend.
All gauges. Values are cluster-wide (every Server replica shares one database
and reports identical numbers) except the per-replica ones flagged below.
Aggregate cluster-wide series with max()/avg(), never sum() (summing
multiplies by the replica count).
| Metric | Labels | Meaning |
|---|---|---|
h3_queue_tasks |
status, capability |
Tasks currently in the DB per status per capability pool. |
h3_queue_oldest_queued_age_seconds |
— | Age of the oldest queued task. Not emitted when nothing is queued. |
h3_queue_oldest_running_age_seconds |
— | Age of the oldest running task; keeps climbing for a stuck task. |
h3_queue_waiting_takeover |
— | Running tasks whose lease is empty/expired, waiting for another replica to take over. Sustained > 0 = every replica for that capability is down or stuck. |
h3_queue_backend_unavailable |
— | Running tasks currently disconnected from SGLang but still retrying within lease. Sustained > 0 = SGLang problem. |
h3_queue_running_on_instance |
h3_instance |
Per-replica. Tasks this replica is executing. Label is h3_instance (not instance) to avoid clashing with Prometheus's own target label. |
h3_queue_completed_24h |
status |
Tasks completed in the last 24h by terminal status. |
h3_queue_backend_up |
capability |
Per-replica. Whether this replica's sidecar SGLang is reachable (1/0). |
h3_queue_backend_jobs |
capability, state |
Per-replica. Jobs on this pod's SGLang by the backend's own state; state="queued" is the backend queue depth. |
h3_queue_build_info |
capability, h3_instance |
Per-replica. Build/deploy info; value is always 1. |
The backend series (h3_queue_backend_up / h3_queue_backend_jobs) are derived by
polling SGLang's GET /v1/videos (SGLang exposes no /metrics), cached briefly
per pod. The poll uses server.config.metricsBackendTimeoutSeconds (default 3s),
which must stay well under serviceMonitor.scrapeTimeout — otherwise a hung
backend makes Prometheus give up and the database-derived series are lost too.
Example scrape:
curl -s http://127.0.0.1:8000/metrics/queue
# h3_queue_tasks{capability="fl2va",status="succeeded"} 12
# h3_queue_backend_up{capability="fl2va"} 1
# h3_queue_backend_jobs{capability="fl2va",state="queued"} 0
# ...Apache License 2.0. See LICENSE.