Files
SolitaryThinker cb46d63f07 [bugfix]: unset GenerationRequest fields inherit model presets
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.
2026-07-11 03:26:33 -07:00

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:

  1. 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.
  2. ServeConfig.default_request (operator-explicit) — projected via explicit_request_updates(); only fields the operator actually wrote into the YAML count as defaults (an explicit null counts as unset). Every other sampling field stays None — "inherit the model preset" — and other sections keep their schema defaults without being pinned.
  3. 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 via ServeConfig.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_latent et al.) — these are derived by the pipeline from ContinuationState, never shipped as request fields.