OBLETHdocs

Video generation

Serve the OpenAI Videos API as jobs that each caller creates, polls, downloads, deletes and lists through the gateway.

4 min readUpdated for v2.4.0Edit on GitHub

You needA backend that implements the OpenAI Videos API
Turn it onRegister the model with model_type: "video"
It costsA flat cost_per_video per created job
Who sees a jobThe key that created it, by default

A video is a job, not one request and one response. The create call returns a job id at once, the backend renders in the background, and the client polls the job until it can download the file.

POST   /v1/videos               create a job (prompt, optional reference image)
GET    /v1/videos/{id}          job status, as the backend reports it
GET    /v1/videos/{id}/content  the finished video
DELETE /v1/videos/{id}          delete the job
GET    /v1/videos               the caller's own jobs

Run a job

  1. 1

    Register the model

    curl -X POST http://localhost:9180/api/v1/models \
      -H "Authorization: Bearer $OBLETH_ADMIN_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "model_name": "video-gen",
        "model_type": "video",
        "upstream_model": "my-video-model",
        "api_base": "https://video.example.internal/v1",
        "cost_per_video": 0.50,
        "enabled": true
      }'
    

    api_base ends in /v1; the gateway appends /videos. You can also set cost_per_video in the model's form on the Models page.

  2. 2

    Create a job

    curl -s http://localhost:8088/v1/videos \
      -H "Authorization: Bearer $OBLETH_KEY" \
      -H "Content-Type: application/json" \
      -d '{"model": "video-gen", "prompt": "A paper boat on a rain-soaked street", "seconds": "4", "size": "1280x720"}'
    

    To send a reference image, use multipart/form-data with OpenAI's input_reference field:

    curl -s http://localhost:8088/v1/videos \
      -H "Authorization: Bearer $OBLETH_KEY" \
      -F model=video-gen -F prompt="The boat sails out of frame" \
      -F input_reference=@first-frame.png
    

    The response is the backend's own video object. The fields a job accepts and the statuses it goes through are the backend's; the gateway does not check them.

  3. 3

    Poll, download and clean up

    curl -s http://localhost:8088/v1/videos/$VIDEO_ID -H "Authorization: Bearer $OBLETH_KEY"
    curl -s http://localhost:8088/v1/videos/$VIDEO_ID/content -H "Authorization: Bearer $OBLETH_KEY" -o video.mp4
    curl -s -X DELETE http://localhost:8088/v1/videos/$VIDEO_ID -H "Authorization: Bearer $OBLETH_KEY"
    curl -s http://localhost:8088/v1/videos -H "Authorization: Bearer $OBLETH_KEY"
    

With the OpenAI SDK:

import time
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8088/v1", api_key="sk_...")

video = client.videos.create(model="video-gen", prompt="A paper boat on a rain-soaked street")
while video.status in ("queued", "in_progress"):
    time.sleep(10)
    video = client.videos.retrieve(video.id)

if video.status == "completed":
    client.videos.download_content(video.id).write_to_file("video.mp4")

What follow-up calls do

The gateway records every job it creates: its id, model, tenant, the key that created it and the endpoint that accepted it. Polls, downloads, deletes and listings are served from that record.

  • They go back to the same endpoint, whatever its health now. If the model has since been disabled or removed the call returns 503; if the endpoint is gone from the model, 502.
  • The backend's answer passes through. A download while the video is still rendering returns whatever not-ready response the backend gives.
  • Downloads stream. The file comes through chunk by chunk with the backend's Content-Type, Content-Length and Content-Disposition.
  • Delete drops the record once the backend confirms (or answers 404). Any other answer keeps it, so you can retry.
  • List asks each backend holding one of your jobs and returns only your entries, newest first. With no jobs you get an empty list.
  • They are free. Follow-ups check the key and tenant but take no slot, reserve no budget and write no usage row.

Who can see a job

OBLETH_VIDEO_JOB_SCOPE (Helm obleth.videoJobScope) decides who may follow up on a job:

ScopePoll, download, deleteGET /v1/videos
key (default)Only the key that created the jobOnly the caller's own jobs
tenantAny key of the same tenantEvery job of the caller's tenant

A job id you don't own returns 404 no such video job, the same as an id that never existed, and the backend is never asked. Another tenant's job is always a 404. Any other value falls back to key with a warning at startup.

Each user is one key: a person with a secret key, or an SSO caller with a per-user identity key. Under key, each user sees only their own videos. Choose tenant when a tenant's keys deliberately share renders. The scope applies at request time, so changing it affects existing jobs immediately.

Under key, a disabled key is refused before the lookup, and a rotated or recreated key is a new key, so neither reaches the old key's jobs. Those jobs finish or expire on the backend.

Billing and admission

A create is charged the model's cost_per_video once the backend accepts the job and the gateway has recorded it, as request type video with no token counts.

  • A create the backend rejects is not charged.
  • A reply with no usable job id returns 502 the upstream did not return a video job id, not charged.
  • If the job cannot be recorded, the gateway deletes it on the backend (best effort) and returns 503, not charged. You never get a job id that was not recorded.
  • A create is never served from the response cache.

The create takes a slot in the model's pool only while the create call runs, so fairshare limits how fast jobs are created, not how many render at once. Budgets reserve and settle on the create like any request. Input guardrails scan the prompt; a reference image is not scanned.

Job records, ids and retention

Records live in the Postgres video_jobs table rather than Redis, because the Redis eviction policy could drop a key with a TTL mid-render. Every gateway replica prunes records older than OBLETH_VIDEO_JOB_RETENTION_HOURS (default 24; Helm obleth.videoJobRetentionHours) once an hour. A follow-up for a pruned job returns 404.

Job ids are the backend's. The gateway accepts any id of 1 to 256 printable ASCII characters except one containing /, \, ?, # or %, or . and .., and forwards it percent-encoded as one path segment.

A multipart create is sent once and never retried. A JSON create follows the model's retry and failover settings. The model's upstream headers go on the create and every follow-up.

Health and what is not handled yet

Video models get no generation probe; like image models, the check is whether the backend's catalog lists them. GET /v1/models reports them as model_type: "video", mode: "video_generation".

Not handled in this release: admission that counts renders in progress, charging on completion, progress webhooks, and other Videos API operations such as remix.