Directly-constructed GenerationRequests marked every schema default as explicit (normalize_generation_request bound the full serialization when _fastvideo_explicit_paths was absent), so request_to_sampling_param overwrote model preset values with schema defaults: FastWan DMD ran 3->50 steps, TurboDiffusion 4->50, stable-audio 100 steps/CFG7 -> 50/CFG1, and 1.3B Wan examples rendered 720x1280x125f with CFG off. Fix at the root: SamplingConfig fields now default to None = "inherit the model preset". None leaves are never bound as explicit paths — for directly-constructed requests (normalize_generation_request) and for parsed YAML/JSON (parse_config treats explicit null as unset, so widening the types cannot let nulls stomp presets either). Dicts emptied by the pruning are dropped too, so None-valued extensions cannot resurface via the live-attribute read in explicit_request_updates. The _SCHEMA_DEFAULT_UPDATES tolerance hack is deleted (an explicitly-set field unsupported by a model's SamplingParam now always fails loudly), and return_state is skipped in the update loop since it is already translated to return_continuation_state. Consumers of raw (pre-resolution) sampling fields are guarded: the streaming server validates operator-pinned default_request.sampling before the multi-minute model boot (run_server) and in build_app; its segment negative_prompt merge preserves an explicit "" clear; the mock server supplies concrete fallbacks; generate_async resolves the preset so its advertised total_steps matches the actual run; the ray-serve gradio demo passes "" to keep its no-negative-prompt default. Stale schema-default claims in video_api/ServeConfig/openai.md docs updated. Tests: test_preset_inheritance.py locks the contract (bare request inherits preset; partial override; explicit value equal to an old schema default wins; negative_prompt None-inherits/""-clears; parsed nulls inherit; None-valued extensions are dropped; unsupported explicit field raises) plus a build_app pin-validation test.
4.5 KiB
OpenAI-compatible HTTP Contract
The stateless FastVideo HTTP server lives at
fastvideo/entrypoints/openai/.
Launch: fastvideo serve --config serve.yaml.
Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/v1/videos/generations |
Synchronous video generation |
GET |
/v1/videos |
List prior jobs held in the in-memory store |
GET |
/v1/videos/{id} |
Job status / result |
GET |
/v1/videos/{id}/content |
Download the MP4 once ready |
POST |
/v1/images/generations |
Synchronous image generation |
GET |
/v1/models |
Enumerate registered models |
GET |
/health |
Liveness probe |
VideoGenerationsRequest shape
Mirrors the OpenAI POST /v1/videos/generations shape:
{
"prompt": "a fox running through snow",
"size": "1024x1536",
"seconds": 5,
"fps": 24,
"num_frames": 121,
"seed": 42,
"num_inference_steps": 8,
"guidance_scale": 1.0,
"negative_prompt": "blurry, low quality",
"input_reference": "/path/to/init.png"
}
SGLang-compatible extensions carried today:
num_inference_steps, guidance_scale, guidance_scale_2,
true_cfg_scale, negative_prompt, enable_teacache, output_path.
Merge precedence
The server builds a GenerationRequest each call using three layers,
highest first:
- Request body (client-explicit) — only fields carried in
request.model_fields_set(Pydantic v2). Unset fields do not count, even if the Pydantic model has a schema default for them. ServeConfig.default_request(operator-explicit) — projected viaexplicit_request_updates(); only fields the operator actually wrote into the YAML count as defaults (an explicitnullcounts as unset). Every other sampling field staysNone— "inherit the model preset" — and other sections keep their schema defaults without being pinned.- Hardcoded fallback — e.g.
fps = 24.
The gate matters: the Pydantic surface carries schema defaults and the
dataclass surface carries non-None defaults outside sampling. Without
model_fields_set / explicit-path tracking, defaults would masquerade
as intent and silently shadow the other side.
See video_api.py::_build_generation_kwargs
for the canonical implementation; the per-request assembly lives there,
not in pipeline code.
Continuation state
The stateless surface accepts an opaque ContinuationState round-trip.
Clients that want continuation pass the prior state blob back on the
next request, and receive a new one on the response when
request.output.return_state = true.
Shape:
{
"state": {
"kind": "ltx2.v1",
"payload": { "schema_version": 1, "segment_index": 3, ... }
}
}
Payload is always JSON-serializable. Large tensors may live in an
opaque blob-store reference the client simply round-trips; see
LTX2ContinuationState.
Continuation is not yet wired all the way through to
generator.generate_video(...) — PR 7.6 (GPU pool upstream) is the
pipeline-level consumer. PR 7 locked the envelope so this surface is
stable ahead of that plumbing.
Error codes
| HTTP | Condition |
|---|---|
400 Bad Request |
Parse/validation failure (unknown field, type mismatch, incompatible preset/state) |
404 Not Found |
GET /v1/videos/{id} for an unknown job |
409 Conflict |
Job id already exists |
500 Internal Server Error |
Pipeline raised; body mirrors upstream OpenAI error envelope |
503 Service Unavailable |
No generator loaded, or shutdown in progress |
Errors include a JSON body with
{"error": {"type": "...", "message": "..."}} matching the OpenAI
Python SDK's expectation.
What does not cross this boundary
- Flat legacy kwargs (
ltx2_refine_enabled,torch_compile_kwargs, etc.) — these are init-time, configured viaServeConfig.generator, never per-request. - Private Dreamverse-only fields — those live in a private adapter on the Dreamverse side; the public FastVideo surface never promises backward compatibility for them.
- Raw tensor payloads (
ltx2_audio_clean_latentet al.) — these are derived by the pipeline fromContinuationState, never shipped as request fields.